diff --git a/.jazzy.yaml b/.jazzy.yaml
deleted file mode 100644
index 8fac6a1..0000000
--- a/.jazzy.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-documentation: "{BinaryFormat,ProtobufSupport}.md"
-author: 'Christoph Hagen'
-author_url: 'https://christophhagen.de'
-source_host: 'github'
-source_host_url: 'https://github.com/christophhagen/BinaryCodable'
diff --git a/BinaryFormat.md b/BinaryFormat.md
index 59cb9df..c599385 100644
--- a/BinaryFormat.md
+++ b/BinaryFormat.md
@@ -1,176 +1,223 @@
# Binary data structure
-**Note:** The `BinaryCodable` format is optimized for size, but does not go all-out to create the smallest binary sizes possible.
+**This document describes the binary format of version 3. For version 2 and below, see [legacy format](LegacyFormat.md)**
+
+**Note:** The `BinaryCodable` format is optimized for size, but does not go all-out to create the smallest binary sizes possible.
The binary format, while being efficient, needs to serve as a general-purpose encoding, which will never be as efficient than a custom format optimized for a very specific use case.
-If this is your goal, then simply using `Codable` with it's key-value approach will not be the best solution. An unkeyed format optimized for the actually encoded data will be more suitable. But if you're really looking into this kind of efficiency, then you probably know this already.
+If this is your goal, then simply using `Codable` with it's key-value approach will not be the best solution.
+An unkeyed format optimized for the actually encoded data will be more suitable.
+But if you're really looking into this kind of efficiency, then you probably know this already.
+
+The encoding format used by `BinaryCodable` has been designed to be efficient, while at the same time supporting all features of the `Codable` implementation.
+
+## Basic types
+
+The basic types known to `Codable` are listed in the following table:
+
+| Type | Length | Encoding |
+| --- | --- | --- |
+| Bool | 1 | false: `0x00`, true: `0x01` |
+| Data | ? | As itself
+| Double | 8 | IEEE Double representation, little endian |
+| Float | 4 |IEEE Double representation, little endian |
+| Int8 | 1 | [Little endian](#little-endian) |
+| Int16 | 2 | [Little endian](#little-endian) |
+| Int32 | 1-5 | Zig-zag variable-length encoding |
+| Int64 | 1-9 | Zig-zag variable-length encoding |
+| Int | 1-9 | Zig-zag variable-length encoding |
+| String | ? | UTF-8 data |
+| UInt8 | 1 | [Little endian](#little-endian) |
+| UInt16 | 2 | [Little endian](#little-endian) |
+| UInt32 | 1-5 | [Variable-length encoding](#variable-length-encoding) |
+| UInt64 | 1-10 | [Variable-length encoding](#variable-length-encoding) |
+| UInt | 1-10 | [Variable-length encoding](#variable-length-encoding) |
+
+`BinaryCodable` also adds the following alternative encodings using the [`FixedSize`](#fixed-size-integers) property wrapper:
+
+| Type | Length | Encoding |
+| --- | --- | --- |
+| FixedSize\ | 4 | [Little endian](#little-endian) |
+| FixedSize\ | 8 | [Little endian](#little-endian) |
+| FixedSize\ | 8 | [Little endian](#little-endian) |
+| FixedSize\ | 4 | [Little endian](#little-endian) |
+| FixedSize\ | 8 | [Little endian](#little-endian) |
+| FixedSize\ | 8 | [Little endian](#little-endian) |
+
+`FixedSize` can be used as a property wrapper, to transparently change the encoding:
-The encoding format used by `BinaryCodable` is similar to Google's [Protocol Buffers](https://developers.google.com/protocol-buffers) in some aspects, but provides much more flexibility regarding the different types which can be encoded, including the ability to encode `Optional`, `Set`, single values, multidimensional `Array`s, and more.
+```swift
+struct MyType: Codable {
-## Integer encoding
+ @FixedSize
+ var userId: Int64
+}
+```
-Integers are encoded with different strategies depending on their size. Smaller types, like `UInt8`, `Int8`, `UInt16`, and `Int16` are encoded using their binary representations in little-endian format.
+### Boolean
-Larger integers, like `UInt32`, `Int32`, `UInt64`, `Int64`, `Int`, and `UInt` are (by default) encoded using variable length zig-zag encoding, similar to [Protobuf signed integers](https://developers.google.com/protocol-buffers/docs/encoding#signed-ints). This means that smaller values are encoded as shorter binary representations, which is useful if integer values are mostly small.
-**Note:** The `Varint` implementation is not equivalent to `Protobuf`, since `BinaryCodable` uses the last byte of a large integer directly, and thus encodes `Int.max` with 9 Byte instead of 10. This encoding is adapted when enforcing protobuf compatibility.
+`Bool` values are encoded as a single byte, using `1` for `true`, and `0` for `false`.
-The property wrapper [`FixedSize`](#fixed-size-integers) forces the values to be encoded using their little-endian representations.
+### Little endian
-## Floating-point types
+Smaller integer types, like `UInt8`, `Int8`, `UInt16`, and `Int16` are encoded using their binary representations in little-endian format.
+This format is chosen since most Apple architectures already store data as little-endian.
-`Float` and `Double` values are encoded using their binary representations in little-endian format.
+### Variable-length encoding
-## Strings
+Larger unsigned integers (`UInt32`, `UInt64` and `UInt`) are (by default) encoded using variable length encoding, similar to [Protobuf Base128 Varints](https://protobuf.dev/programming-guides/encoding/#varints).
+This means that smaller values are encoded as shorter binary representations, which is useful if integer values are mostly small.
-Swift `String` values are encoded using their `UTF-8` representations. If a string can't be encoded this way, then encoding fails.
+**Note:** The `Varint` implementation is not equivalent to `Protobuf`, since `BinaryCodable` uses the last byte of a large integer directly (no continuation bit), and thus encodes `Int.max` with 9 Byte instead of 10.
-## Booleans
+### Zig-zag encoding
-`Bool` values are always encoded as a single byte, using `1` for `true`, and `0` for `false`.
+Larger signed integers (`Int32`, `Int64` and `Int`) are (by default) encoded using zig-zag encoding, similar to [Protobuf signed integers](https://developers.google.com/protocol-buffers/docs/encoding#signed-ints).
+This format is more efficient for integers where the magnitude is small.
-## Arrays
+### Fixed-size integers
-Arrays (and other sequences) are encoded by converting each item to binary data, and concatenating the results. Elements with variable length (like `String`) are prepended with their length encoded as a [Varint](#integer-encoding).
+The property wrapper `FixedSize` forces the wrapped values to be encoded using their little-endian representations.
+This is useful if the integer values are often large, e.g. for random numbers.
-### Arrays of Optionals
+### Floating-point types
-It is possible to encode arrays where the elements are `Optional`, e.g. `[Bool?]`.
-For all types with compiler-generated `Codable` conformance, an optional is prepended with one byte (`1` for the `.some()` case, `0` for `nil`).
-If the optional has a value, then the encoded data follows the `1` byte.
-If `nil` is encoded, then no additional data is added.
-This means that an array of `[Bool?]` with the values `[true, nil, false]` is encoded as `[1, 1, 0, 1, 0]`.
+`Float` and `Double` values are encoded using their binary representations in little-endian format.
-- Note: One benefit of this encoding is that top-level sequences can be joined using their binary data, where `encoded([a,b]) | encoded([c,d]) == encoded([a,b,c,d])`.
+### Strings
-Custom implementations of `Encodable` and `Decodable` can directly call `encodeNil()` on `UnkeyedEncodingContainer` and `decodeNil()` on `UnkeyedDecodingContainer`.
-This feature is not supported in the standard configuration and will result in a fatal error.
-If these functions are needed, then the `prependNilIndexSetForUnkeyedContainers` must be set for the encoder and decoder.
-If this option is set to `true`, then each unkeyed container is prepended with a "nil index set".
-It first consists of the number of `nil` elements in the sequence (only those encoded using `encodeNil()`), encoded as a `Varint`.
-Then follow the indices in the array where `nil` values are present, each encoded as a `Varint`.
-The decoder can then first parse this `nil` set, and return the appropriate value for each position where a `nil` value is encoded when `decodeNil()` is called.
-This approach is fairly efficient while only few `nil` values are encoded, or while the sequence doesn't contain a large number of elements.
-For arrays that don't contain optionals, only a single byte (`0`) is prepended to the binary representation, to signal that there are no `nil` indices in the sequence.
+Swift `String` values are encoded using their `UTF-8` representations.
+If a string can't be encoded this way, then encoding fails.
-More efficient ways could be devised to handle arrays of optionals, like specifying the number of `nil` or non-nil elements following one another, but the increased encoding and decoding complexity don't justify these gains in communication efficiency.
+## Containers
-## Structs
+Every `Codable` structure is in some way constructed from just a the described basic types, which are arranged in three different containers:
+
+**Single value containers** can contain only a single element, or alternatively `nil`.
-Structs are encoded using `Codable`'s `KeyedEncodingContainer`, which uses `String` or `Int` coding keys to distinguish the properties of the types.
-By default, Swift uses the property names as `String` keys, which are used to encode each property as a key-value pair on the wire.
-The first value is a `Varint`, which contains the length of the string key, plus additional information about the data associated with the key.
-The bits 0-2 are used to signal the size value, and bit 3 of the `Varint` indicates whether the key is a string key (`1` = string, `0` = int).
-The following data types are possible:
+**Unkeyed containers** can store a sequence of different values (including `nil` values).
-| Data type | Raw value | Protobuf | Swift types | Description |
-| :---------------------- | :-------- | :---------------- | :------------------------------------------------------------------------------------- |
-| `variableLengthInteger` | `0` | `varint`/`zigzag` | `Int`, `Int32`, `Int64`, `UInt`, `UInt32`, `UInt64` | A Base128 `Varint` using 1-9 bytes of data |
-| `eightBytes` | `1` | `fixed64bit` | `Double`, `FixedSize`, `FixedSize`, `FixedSize`, `FixedSize` | A 64-bit float or integer in little-endian encoding. |
-| `variableLength` | `2` | `delimited` | `String`, `Struct`, ... | The length of the data encoded as a `Varint` followed by `length` bytes |
-| `fourBytes` | `5` | `fixed32bit` | `Float`, `FixedSize`, `FixedSize` | A 32-bit float or integer in little-endian encoding. |
-| `byte` | `6` | - | `Bool`, `UInt8`, `Int8` | A single byte storing a number or boolean |
-| `twoBytes` | `7` | - | `Int16`, `UInt16` | Two bytes storing an integer using little-endian format |
+**Keyed containers** consist of values, which are associated with `CodingKey`s (either a String or an Int value).
+Each of these containers can either encode [basic types](#basic-types), or include nested containers.
+No matter what is encoded, each element is usually preceded by a length indicator, to show the decoder where the container ends.
-With the four lower bits occupied by the data type and the string key indicator, the remaining bits are left to encode the length of the string key.
+### Single Value Container encoding
-For example, a property named `xyz` of type `UInt8` with value `123` would be encoded to the following:
+Since a single value container can contain `nil`, it's encoding always requires a `nil` indicator.
+At the top level, this is a single byte of either `1` to indicate `nil` (with no additional data folling it), or `0`, to indicate that the encoded value follows.
+If a single value container is nested in some other container, then the `nil` indicator is contained in the length information that is prepended to the container data.
-| Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
-| :----------------------------------------- | :--------- | :--------- | :--------- | :--------- |
-| `0` `011` `1` `110` | `01111000` | `01111001` | `01111010` | `01111011` |
-| Length `3`, `String` key, Data type `byte` | `x` | `y` | `z` | `123` |
+### Unkeyed containers
-### Integer keys
+Unkeyed containers can contain values of different types in a sequence.
+Each value (other containers or basic types) is encoded using its encoded data, prefixed by a length indicator to signal the decoder how many bytes are associated with each value.
+This enables the decoder to split the data into separate chunks before the actual types are decoded.
-The Swift `Codable` framework also provides the ability to specify integer keys for properties, which can significantly reduce the binary size. Integer keys can be assigned to properties by implementing custom `CodingKeys` for a type:
-```swift
-struct MyCodable: Codable {
+### Length/nil indicators
- let xyz: UInt8
+The length indicators for each encoded value in an unkeyed container consists of an unsigned integer, encoded using [variable-length encoding](#variable-length-encoding).
+The length is not directly encoded as an integer, but shifted to the left by one bit, so that the LSB can be used as a `nil` indicator.
+If the LSB is set, then `nil` is encoded for the value, and all other bits must be unset.
+If the LSB is set to `0`, then the value is not `nil`, and the integer indicates the number of bytes used by the value's encoded data.
- enum CodingKeys: Int, CodingKey {
- case xyz = 2
- }
-}
+For example assume the following encoding routine:
+
+```swift
+var container = encoder.unkeyedContainer()
+try container.encode(false)
+try container.encodeNil()
+try container.encode("Hello")
+try container.encode(Data())
```
-Integer coding keys are encoded as `Varint` instead of the `String` key length. This results in the following encoding for the same example as before:
-| Byte 0 | Byte 1 |
-| :------------------------------------------- | :--------- |
-| `0` `010` `0` `110` | `01111011` |
-| Integer key `2`, `Int` key, Data type `byte` | `123` |
+This yields the following binary representation (hex):
-Evidently this is a significant improvement, especially for long property names. Note that while it is possible to specify any integer as the key (between 2^59 and -2^59), small, positive integers are the most efficient.
+```
+02 00 01 14 48 65 6C 6C 6F 00
+```
-### Optional properties
+which translates to:
-Any properties of structs or other keyed containers is omitted from the binary data, i.e. nothing is encoded.
-The absence of a key then indicates to the decoder that the value is `nil`.
-For multiple optionals (e.g. `Bool??`), the inner optional is encoded as a `varLen` type, with the same encoding as optionals in arrays.
+```
+0x02 // First element is not nil, length 1
+0x00 // First element data (Bool `false`)
+0x01 // Second element is nil
+0x14 // Third element is not nil, length 5
+0x48656C6C6F // Third element data, String "Hello"
+0x00 // Fourth element is not nil, length 0
+```
-## Codable quirks
+### Keyed containers
-There are some interesting details on how `Codable` treats certain types, which produce unexpected binary data. Although `BinaryCodable` decodes all these cases correctly, implementing these "features" may be difficult on other platforms.
+Keyed containers work similar to unkeyed containers, except that each element also has a key inserted before the element data.
-### Enums with associated values
+Keys can be of type `String` or `Int`.
+For both types, and unsigned integer is encoded using [variable-length encoding](#variable-length-encoding).
+If the key is an integer, then the LSB is set to `0`, and the remaining value (shifted by 1 to remove the LSB) indicates the value of the integer key.
+So an integer key of `1` is encoded as `0x02`, a key of `2` as `0x04` and so on.
+This process is very similar to the encoding of the length or `nil` described in the previous chapter.
-Given an enum with associated values:
+If the key is a string, then the LSB is set to `1`, and the remaining integer specifies the length of the string key.
+The actual string is then appended in its [encoded form](#strings).
+So a string key of "value" is encoded as:
-```swift
-enum MyEnum: Codable {
- case one(String)
- case two(Bool, Data)
-}
```
-
-The encoding will consist of:
-- The enum case name as a String key
-- A keyed container with:
- - All associated values in the order of their definition, keyed by `"_0"`, `"_1"`, ...
-
-For example, the value
-
-```swift
-let value = MyEnum.one("Some")
+11 // String key, length 5
+118, 97, 108, 117, 101 // String "value"
```
-would be encoded as:
+After the key data follows the actual element data, consisting (similar to unkeyed containers) of the length/nil indicator and the encoded value.
-| Byte 0 | Byte 1 - 3 | Byte 4 | Byte 5 | Byte 6 - 7 | Byte 8 | Byte 9 - 12 |
-| :------------------------------ | :--------------- | :--------- | :----------------- | :---------- | :--------- | :-------------------- |
-| `0x3A` | `0x6F 0x6E 0x65` | `0x08` | `0x2A` | `0x5F 0x30` | `0x04` | `0x53 0x6E 0x64 0x08` |
-| `String` key (Len 3), `VarLen` | `one` | Length `8` | String key (Len 2) | `_0` | Length `4` | `Some` |
+### Basic example
-Let's use the same example with integer keys:
+Consider the following swift structure:
```swift
-enum MyEnum: Codable {
- case one(String)
- case two(Bool, UInt8)
+struct Message: Codable {
+ let isComplete: Bool
+ let owner: String?
+ let references: [Int]
enum CodingKeys: Int, CodingKey {
- case one = 1
- case two = 2
+ case isComplete = 1
+ case owner = 2
+ case references = 3
}
}
```
-Then, the value
+Encode the message
```swift
-let value = MyEnum.two(true, 123)
+let message = Message(
+ isComplete: true
+ owner: "Bob"
+ references: [3, -280])
```
-would be encoded as:
-
-| Byte 0 | Byte 1 | Byte 2 | Byte 3 - 4 | Byte 5 | Byte 6 | Byte 7 - 8 | Byte 9 |
-| :----------------------- | :--------- | :------------------------- | :---------- | :----------- | :------------------------- | :---------- | :----------- |
-| `0x22` | `0x08` | `0x2E` | `0x5F 0x30` | `0x01` | `0x2E` | `0x5F 0x31` | `0x7B` |
-| `Int` key (2), `VarLen` | Length `8` | String key (Len 2), `Byte` | `_0` | `Bool(true)` | String key (Len 2), `Byte` | `_1` | `UInt8(123)` |
-
-Note: Since the associated values are encoded in a keyed container, there order in the binary data may be different, unless the `sortKeysDuringEncoding` option is set to `true`.
-
-### Special dictionaries
+will give the following binary data:
+
+| Byte index | Value | Content |
+|--- |--- |--- |
+| 0 | 0x02 | CodingKey(stringValue: 'isComplete', intValue: 1)
+| 1 | 0x02 | Length 1
+| 2 | 0x01 | Bool `true`
+| 3 | 0x04 | CodingKey(stringValue: 'owner', intValue: 2)
+| 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`
+
+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
+
+### Dictionaries
Most dictionaries are treated as `Unkeyed Containers` by `Codable`, and each key value pair is encoded by first encoding the key, followed by the value, thus creating the flat structure:
@@ -179,7 +226,7 @@ Most dictionaries are treated as `Unkeyed Containers` by `Codable`, and each key
#### Dictionaries with Integer keys
-For all dictionaries, which use `Int` as the key, e.g. `[Int: String]`, `[Int: Int]`, or generally `[Int: ...]`, the encoding is done using a `Keyed` container, where each dictionary value is encoded using a `CodingKey` with an integer value. This results in a structure more resembling [struct encoding](#structs) with [integer keys](#integer-keys):
+For all dictionaries using `Int` as the key, e.g. `[Int: String]`, `[Int: Int]`, or generally `[Int: ...]`, the encoding is done using a `Keyed` container, where each dictionary value is encoded using a `CodingKey` with an integer value. This results in a structure more resembling [struct encoding](#structs) with [integer keys](#integer-keys):
| Byte(s) | Byte(s) | Byte(s) | Byte(s) | Byte(s) | Byte(s) |
| :-------------------------- | :------ | :-------------------------- | :------ | :-------------------------- | :------ |
@@ -220,15 +267,5 @@ For dictionaries with `String` keys (`[String: ...]`), the process is similar to
## Stream encoding
-The encoding for data streams only differs from standard encoding in two key aspects.
-
-### Added length information
-
-Each top-level element is encoded as if it is part of an unkeyed container (which it essentially is), meaning that each element has the necessary length information prepended to determine it's size.
-Only types with data type `variable length` have their length prepended using [varint](#integer-encoding) encoding.
-This concerns `String` and `Data`, as well as complex types like structs and arrays, among others.
-
-### Optionals
-
-A single byte is prepended to each `Optional` element, where binary `0x01` is used to indicate a non-optional value, and `0x00` is used to signal an optional value.
-`nil` values have no additional data, so each is encoded using one byte.
+The encoding for data streams only differs from standard encoding in one key aspect.
+Each top-level element is encoded as if it is part of an unkeyed container (which it essentially is), meaning that each element has the necessary length information prepended to determine its size.
\ No newline at end of file
diff --git a/LegacyFormat.md b/LegacyFormat.md
new file mode 100644
index 0000000..59cb9df
--- /dev/null
+++ b/LegacyFormat.md
@@ -0,0 +1,234 @@
+# Binary data structure
+
+**Note:** The `BinaryCodable` format is optimized for size, but does not go all-out to create the smallest binary sizes possible.
+The binary format, while being efficient, needs to serve as a general-purpose encoding, which will never be as efficient than a custom format optimized for a very specific use case.
+If this is your goal, then simply using `Codable` with it's key-value approach will not be the best solution. An unkeyed format optimized for the actually encoded data will be more suitable. But if you're really looking into this kind of efficiency, then you probably know this already.
+
+The encoding format used by `BinaryCodable` is similar to Google's [Protocol Buffers](https://developers.google.com/protocol-buffers) in some aspects, but provides much more flexibility regarding the different types which can be encoded, including the ability to encode `Optional`, `Set`, single values, multidimensional `Array`s, and more.
+
+## Integer encoding
+
+Integers are encoded with different strategies depending on their size. Smaller types, like `UInt8`, `Int8`, `UInt16`, and `Int16` are encoded using their binary representations in little-endian format.
+
+Larger integers, like `UInt32`, `Int32`, `UInt64`, `Int64`, `Int`, and `UInt` are (by default) encoded using variable length zig-zag encoding, similar to [Protobuf signed integers](https://developers.google.com/protocol-buffers/docs/encoding#signed-ints). This means that smaller values are encoded as shorter binary representations, which is useful if integer values are mostly small.
+**Note:** The `Varint` implementation is not equivalent to `Protobuf`, since `BinaryCodable` uses the last byte of a large integer directly, and thus encodes `Int.max` with 9 Byte instead of 10. This encoding is adapted when enforcing protobuf compatibility.
+
+The property wrapper [`FixedSize`](#fixed-size-integers) forces the values to be encoded using their little-endian representations.
+
+## Floating-point types
+
+`Float` and `Double` values are encoded using their binary representations in little-endian format.
+
+## Strings
+
+Swift `String` values are encoded using their `UTF-8` representations. If a string can't be encoded this way, then encoding fails.
+
+## Booleans
+
+`Bool` values are always encoded as a single byte, using `1` for `true`, and `0` for `false`.
+
+## Arrays
+
+Arrays (and other sequences) are encoded by converting each item to binary data, and concatenating the results. Elements with variable length (like `String`) are prepended with their length encoded as a [Varint](#integer-encoding).
+
+### Arrays of Optionals
+
+It is possible to encode arrays where the elements are `Optional`, e.g. `[Bool?]`.
+For all types with compiler-generated `Codable` conformance, an optional is prepended with one byte (`1` for the `.some()` case, `0` for `nil`).
+If the optional has a value, then the encoded data follows the `1` byte.
+If `nil` is encoded, then no additional data is added.
+This means that an array of `[Bool?]` with the values `[true, nil, false]` is encoded as `[1, 1, 0, 1, 0]`.
+
+- Note: One benefit of this encoding is that top-level sequences can be joined using their binary data, where `encoded([a,b]) | encoded([c,d]) == encoded([a,b,c,d])`.
+
+Custom implementations of `Encodable` and `Decodable` can directly call `encodeNil()` on `UnkeyedEncodingContainer` and `decodeNil()` on `UnkeyedDecodingContainer`.
+This feature is not supported in the standard configuration and will result in a fatal error.
+If these functions are needed, then the `prependNilIndexSetForUnkeyedContainers` must be set for the encoder and decoder.
+If this option is set to `true`, then each unkeyed container is prepended with a "nil index set".
+It first consists of the number of `nil` elements in the sequence (only those encoded using `encodeNil()`), encoded as a `Varint`.
+Then follow the indices in the array where `nil` values are present, each encoded as a `Varint`.
+The decoder can then first parse this `nil` set, and return the appropriate value for each position where a `nil` value is encoded when `decodeNil()` is called.
+This approach is fairly efficient while only few `nil` values are encoded, or while the sequence doesn't contain a large number of elements.
+For arrays that don't contain optionals, only a single byte (`0`) is prepended to the binary representation, to signal that there are no `nil` indices in the sequence.
+
+More efficient ways could be devised to handle arrays of optionals, like specifying the number of `nil` or non-nil elements following one another, but the increased encoding and decoding complexity don't justify these gains in communication efficiency.
+
+## Structs
+
+Structs are encoded using `Codable`'s `KeyedEncodingContainer`, which uses `String` or `Int` coding keys to distinguish the properties of the types.
+By default, Swift uses the property names as `String` keys, which are used to encode each property as a key-value pair on the wire.
+The first value is a `Varint`, which contains the length of the string key, plus additional information about the data associated with the key.
+The bits 0-2 are used to signal the size value, and bit 3 of the `Varint` indicates whether the key is a string key (`1` = string, `0` = int).
+The following data types are possible:
+
+| Data type | Raw value | Protobuf | Swift types | Description |
+| :---------------------- | :-------- | :---------------- | :------------------------------------------------------------------------------------- |
+| `variableLengthInteger` | `0` | `varint`/`zigzag` | `Int`, `Int32`, `Int64`, `UInt`, `UInt32`, `UInt64` | A Base128 `Varint` using 1-9 bytes of data |
+| `eightBytes` | `1` | `fixed64bit` | `Double`, `FixedSize`, `FixedSize`, `FixedSize`, `FixedSize` | A 64-bit float or integer in little-endian encoding. |
+| `variableLength` | `2` | `delimited` | `String`, `Struct`, ... | The length of the data encoded as a `Varint` followed by `length` bytes |
+| `fourBytes` | `5` | `fixed32bit` | `Float`, `FixedSize`, `FixedSize` | A 32-bit float or integer in little-endian encoding. |
+| `byte` | `6` | - | `Bool`, `UInt8`, `Int8` | A single byte storing a number or boolean |
+| `twoBytes` | `7` | - | `Int16`, `UInt16` | Two bytes storing an integer using little-endian format |
+
+
+With the four lower bits occupied by the data type and the string key indicator, the remaining bits are left to encode the length of the string key.
+
+For example, a property named `xyz` of type `UInt8` with value `123` would be encoded to the following:
+
+| Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
+| :----------------------------------------- | :--------- | :--------- | :--------- | :--------- |
+| `0` `011` `1` `110` | `01111000` | `01111001` | `01111010` | `01111011` |
+| Length `3`, `String` key, Data type `byte` | `x` | `y` | `z` | `123` |
+
+### Integer keys
+
+The Swift `Codable` framework also provides the ability to specify integer keys for properties, which can significantly reduce the binary size. Integer keys can be assigned to properties by implementing custom `CodingKeys` for a type:
+```swift
+struct MyCodable: Codable {
+
+ let xyz: UInt8
+
+ enum CodingKeys: Int, CodingKey {
+ case xyz = 2
+ }
+}
+```
+Integer coding keys are encoded as `Varint` instead of the `String` key length. This results in the following encoding for the same example as before:
+
+| Byte 0 | Byte 1 |
+| :------------------------------------------- | :--------- |
+| `0` `010` `0` `110` | `01111011` |
+| Integer key `2`, `Int` key, Data type `byte` | `123` |
+
+Evidently this is a significant improvement, especially for long property names. Note that while it is possible to specify any integer as the key (between 2^59 and -2^59), small, positive integers are the most efficient.
+
+### Optional properties
+
+Any properties of structs or other keyed containers is omitted from the binary data, i.e. nothing is encoded.
+The absence of a key then indicates to the decoder that the value is `nil`.
+For multiple optionals (e.g. `Bool??`), the inner optional is encoded as a `varLen` type, with the same encoding as optionals in arrays.
+
+## Codable quirks
+
+There are some interesting details on how `Codable` treats certain types, which produce unexpected binary data. Although `BinaryCodable` decodes all these cases correctly, implementing these "features" may be difficult on other platforms.
+
+### Enums with associated values
+
+Given an enum with associated values:
+
+```swift
+enum MyEnum: Codable {
+ case one(String)
+ case two(Bool, Data)
+}
+```
+
+The encoding will consist of:
+- The enum case name as a String key
+- A keyed container with:
+ - All associated values in the order of their definition, keyed by `"_0"`, `"_1"`, ...
+
+For example, the value
+
+```swift
+let value = MyEnum.one("Some")
+```
+
+would be encoded as:
+
+| Byte 0 | Byte 1 - 3 | Byte 4 | Byte 5 | Byte 6 - 7 | Byte 8 | Byte 9 - 12 |
+| :------------------------------ | :--------------- | :--------- | :----------------- | :---------- | :--------- | :-------------------- |
+| `0x3A` | `0x6F 0x6E 0x65` | `0x08` | `0x2A` | `0x5F 0x30` | `0x04` | `0x53 0x6E 0x64 0x08` |
+| `String` key (Len 3), `VarLen` | `one` | Length `8` | String key (Len 2) | `_0` | Length `4` | `Some` |
+
+Let's use the same example with integer keys:
+
+```swift
+enum MyEnum: Codable {
+ case one(String)
+ case two(Bool, UInt8)
+
+ enum CodingKeys: Int, CodingKey {
+ case one = 1
+ case two = 2
+ }
+}
+```
+
+Then, the value
+
+```swift
+let value = MyEnum.two(true, 123)
+```
+
+would be encoded as:
+
+| Byte 0 | Byte 1 | Byte 2 | Byte 3 - 4 | Byte 5 | Byte 6 | Byte 7 - 8 | Byte 9 |
+| :----------------------- | :--------- | :------------------------- | :---------- | :----------- | :------------------------- | :---------- | :----------- |
+| `0x22` | `0x08` | `0x2E` | `0x5F 0x30` | `0x01` | `0x2E` | `0x5F 0x31` | `0x7B` |
+| `Int` key (2), `VarLen` | Length `8` | String key (Len 2), `Byte` | `_0` | `Bool(true)` | String key (Len 2), `Byte` | `_1` | `UInt8(123)` |
+
+Note: Since the associated values are encoded in a keyed container, there order in the binary data may be different, unless the `sortKeysDuringEncoding` option is set to `true`.
+
+### Special dictionaries
+
+Most dictionaries are treated as `Unkeyed Containers` by `Codable`, and each key value pair is encoded by first encoding the key, followed by the value, thus creating the flat structure:
+
+| Key 1 | Value 1 | Key 2 | Value 2 | Key 3 | Value 3 |
+| :---- | :------ | :---- | :------ | :---- | :------ |
+
+#### Dictionaries with Integer keys
+
+For all dictionaries, which use `Int` as the key, e.g. `[Int: String]`, `[Int: Int]`, or generally `[Int: ...]`, the encoding is done using a `Keyed` container, where each dictionary value is encoded using a `CodingKey` with an integer value. This results in a structure more resembling [struct encoding](#structs) with [integer keys](#integer-keys):
+
+| Byte(s) | Byte(s) | Byte(s) | Byte(s) | Byte(s) | Byte(s) |
+| :-------------------------- | :------ | :-------------------------- | :------ | :-------------------------- | :------ |
+| `Int` Key(Key 1), Data type | Value 1 | `Int` Key(Key 2), Data type | Value 2 | `Int` Key(Key 3), Data type | Value 3 |
+
+For example, the following works:
+
+```swift
+struct MyStruct: Codable {
+ let a: Int
+ let b: Int
+ let c: Int
+}
+
+// Encode a dictionary
+let input: [String: Int] = ["a" : 123, "b": 0, "c": -123456]
+let data = try BinaryEncoder.encode(input)
+
+// Decode as struct
+let decoded = try BinaryDecoder.decode(MyStruct.self, from: data)
+```
+
+It also works the other way round:
+```swift
+// Encode struct
+let input = MyStruct(a: 123, b: 0, c: -123456)
+let data = try BinaryEncoder.encode(input)
+
+// Decode as dictionary
+let decoded: [String: Int] = try BinaryDecoder.decode(from: data)
+```
+
+Note that this only works for dictionaries with concrete `Encodable` values, e.g. `[String: Encodable]` won't work.
+
+#### Dictionaries with String keys
+
+For dictionaries with `String` keys (`[String: ...]`), the process is similar to the above, except with `CodingKey`s having the `stringValue` of the key. There is another weird exception though: Whenever a `String` can be represented by an integer (i.e. when `String(key) != nil`), then the corresponding `CodingKey` will have its `integerValue` also set. This means that for dictionaries with integer keys, there may be a mixture of integer and string keys present in the binary data, depending on the input values. But don't worry, `BinaryCodable` will also handle these cases correctly.
+
+## Stream encoding
+
+The encoding for data streams only differs from standard encoding in two key aspects.
+
+### Added length information
+
+Each top-level element is encoded as if it is part of an unkeyed container (which it essentially is), meaning that each element has the necessary length information prepended to determine it's size.
+Only types with data type `variable length` have their length prepended using [varint](#integer-encoding) encoding.
+This concerns `String` and `Data`, as well as complex types like structs and arrays, among others.
+
+### Optionals
+
+A single byte is prepended to each `Optional` element, where binary `0x01` is used to indicate a non-optional value, and `0x00` is used to signal an optional value.
+`nil` values have no additional data, so each is encoded using one byte.
diff --git a/Package.swift b/Package.swift
index 01c65d6..2240f9c 100644
--- a/Package.swift
+++ b/Package.swift
@@ -11,7 +11,7 @@ let package = Package(
targets: ["BinaryCodable"]),
],
dependencies: [
- .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.19.0"),
+ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
],
targets: [
.target(
@@ -19,8 +19,7 @@ let package = Package(
dependencies: []),
.testTarget(
name: "BinaryCodableTests",
- dependencies: ["BinaryCodable", .product(name: "SwiftProtobuf", package: "swift-protobuf")],
- exclude: ["Proto/TestTypes.proto"]),
+ dependencies: ["BinaryCodable"]),
],
swiftLanguageVersions: [.v5]
)
diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift
index 19ff79a..067b630 100644
--- a/Package@swift-5.5.swift
+++ b/Package@swift-5.5.swift
@@ -9,17 +9,14 @@ let package = Package(
name: "BinaryCodable",
targets: ["BinaryCodable"]),
],
- dependencies: [
- .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.19.0"),
- ],
+ dependencies: [],
targets: [
.target(
name: "BinaryCodable",
dependencies: []),
.testTarget(
name: "BinaryCodableTests",
- dependencies: ["BinaryCodable", .product(name: "SwiftProtobuf", package: "swift-protobuf")],
- exclude: ["Proto/TestTypes.proto"]),
+ dependencies: ["BinaryCodable"]),
],
swiftLanguageVersions: [.v5]
)
diff --git a/ProtobufSupport.md b/ProtobufSupport.md
deleted file mode 100644
index d11dee4..0000000
--- a/ProtobufSupport.md
+++ /dev/null
@@ -1,254 +0,0 @@
-# Protocol Buffer Compatibility
-
-`BinaryCodable` provides limited compatibility to [Google Protocol Buffers](https://developers.google.com/protocol-buffers). Certain Swift types can be encoded to protobuf compatible binary data, and vice versa. The standard [binary format](BinaryFormat.md) is similar to protobuf, but includes some deviations to support all Swift types and features. There are additional `ProtobufEncoder` and `ProtobufDecoder` types which change the encoding format to be protobuf-compatible, at the expense of errors for unsupported features.
-
-For a description of the Protocol Buffer format, see the [official documentation](https://developers.google.com/protocol-buffers).
-
-**Important notes**
-- Advanced protobuf features like message concatenation are not supported.
-- Unsupported features of Protobuf *may* cause the encoding to fail with a `ProtobufEncodingError`. Interoperability should be thoroughly checked through testing.
-
-## Usage
-
-The conversion process is equivalent to the `BinaryEncoder` and `BinaryDecoder` types.
-
-```swift
-import BinaryCodable
-```
-
-### Encoding
-
-Construct an encoder when converting instances to binary data, and feed the message(s) into it:
-
-```swift
-let message: Message = ...
-
-let encoder = ProtobufEncoder()
-let data = try encoder.encode(message)
-```
-
-### Decoding
-
-Decoding instances from binary data works much the same way:
-
-```swift
-let decoder = ProtobufDecoder()
-let message = try decoder.decode(Message.self, from: data)
-```
-
-Alternatively, the type can be inferred:
-
-```swift
-let message: Message = try decoder.decode(from: data)
-```
-
-### Errors
-
-It is possible for both encoding and decoding to fail.
-All possible errors occuring during encoding produce `BinaryEncodingError` or `ProtobufEncodingError` errors, while unsuccessful decoding produces `BinaryDecodingError` or `ProtobufDecodingError`.
-All are enums with several cases describing the nature of the error.
-See the documentation of the types to learn more about the different error conditions.
-
-## Message definition
-
-Protobuf organizes data into messages, which are structures with keyed fields. Compatible Swift types must be similar, so either `struct` or `class`. Simple types like `Int` or `Bool` are not supported on the root level, and neither are `Dictionary`, `Array` or `enum`. It's best to think of the proto definitions and construct Swift types in the same way. Let's look at an example from the [Protocol Buffer documentation](https://developers.google.com/protocol-buffers/docs/proto3#simple):
-
-```proto
-message SearchRequest {
- string query = 1;
- int32 page_number = 2;
- int32 result_per_page = 3;
-}
-```
-
-The corresponding Swift definition would be:
-
-```swift
-struct SearchRequest: Codable {
-
- var query: String
-
- var pageNumber: Int32
-
- var resultPerPage: Int32
-
- enum CodingKeys: Int, CodingKey {
- case query = 1
- case pageNumber = 2
- case resultPerPage = 3
- }
-}
-```
-
-The general structure of the messages is very similar, with the proto field numbers specified as integer coding keys.
-
-### Assigning integer keys
-
-The assignment of integer keys follow the same [rules](https://developers.google.com/protocol-buffers/docs/proto3#assigning_field_numbers) as for field numbers, just written out as an `enum` with `RawValue == Int` on the type conforming to `CodingKey`. The smallest field number you can specify is `1`, and the largest is `2^29 - 1`, or `536,870,911`. Codable types without (or with invalid) integer keys can't be encoded using `ProtobufEncoder` and will throw an error.
-
-### Scalar value types
-
-There are several [scalar types](https://developers.google.com/protocol-buffers/docs/proto3#scalar) defined for Protocol Buffers, which are the basic building blocks of messages. `BinaryCodable` provides Swift equivalents for each of them:
-
-| Protobuf primitive | Swift equivalent | Comment |
-| :----------------- | :--------------------- | :-------------------------------------------------------------------- |
-| `double` | `Double` | Always 8 byte |
-| `float` | `Float` | Always 4 byte |
-| `int32` | `Int32` | Uses variable-length encoding |
-| `int64` | `Int64` | Uses variable-length encoding |
-| `uint32` | `UInt32` | Uses variable-length encoding |
-| `uint64` | `UInt64` | Uses variable-length encoding |
-| `sint32` | `SignedInteger` | Uses ZigZag encoding, see [`SignedInteger` wrapper](#signed-integers) |
-| `sint64` | `SignedInteger` | Uses ZigZag encoding, see [`SignedInteger` wrapper](#signed-integers) |
-| `fixed32` | `FixedSize` | See [`FixedSize` wrapper](#fixed-size-integers) |
-| `fixed64` | `FixedSize` | See [`FixedSize` wrapper](#fixed-size-integers) |
-| `sfixed32` | `FixedSize` | See [`FixedSize` wrapper](#fixed-size-integers) |
-| `sfixed64` | `FixedSize` | See [`FixedSize` wrapper](#fixed-size-integers) |
-| `bool` | `Bool` | Always 1 byte |
-| `string` | `String` | Encoded using UTF-8 |
-| `bytes` | `Data` | Encoded as-is |
-| `message` | `struct` | Nested messages are also supported. |
-| `repeated` | `Array` | Scalar values must always be `packed` (the proto3 default) |
-| `enum` | `Enum` | See [Enums](#enums) |
-| `oneof` | `Enum` | See [OneOf Definition](#oneof) |
-
-The Swift types `Int8`, `UInt8`, `Int16`, and `UInt16` are **not** supported, and will result in an error.
-
-Note: `Int` and `UInt` values are always encoded as 64-bit numbers, despite the fact that they might be 32-bit values on some systems. Decoding a 64-bit value on a 32-bit system will result in an error.
-
-### Property wrappers
-
-The Protocol Buffer format provides several different encoding strategies for integers to minimize the binary size depending on the encoded values. By default, all integers are encoded using [Base 128 Varints](https://developers.google.com/protocol-buffers/docs/encoding#varints), but this can be changed using Swift `PropertyWrappers`. The following encoding options exist:
-
-| Swift type | [Varint encoding](https://developers.google.com/protocol-buffers/docs/encoding#varints) | [ZigZag Encoding](https://developers.google.com/protocol-buffers/docs/encoding#signed-ints) | [Fixed-size encoding](https://developers.google.com/protocol-buffers/docs/encoding#non-varint_numbers) |
-| :--------- | :-------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------- |
-| `Int32` | `Int32` | `SignedInteger` | `FixedSize` |
-| `Int64` | `Int64` | `SignedInteger` | `FixedSize` |
-| `UInt32` | `UInt32` | - | `FixedSize` |
-| `UInt64` | `UInt64` | - | `FixedSize` |
-
-#### Fixed size integers
-
-While varints are efficient for small numbers, their encoding introduces a storage and computation penalty when the integers are often large, e.g. for random numbers. `BinaryCodable` provides the `FixedSize` wrapper, which forces integers to be encoded using their little-endian binary representations. This means that e.g. an `Int32` is always encoded as 4 byte (instead of 1-5 bytes using Varint encoding). This makes 32-bit `FixedSize` types more efficient than `Varint` if values are often larger than `2^28` (`2^56` for 64-bit types).
-
-Use the property wrapper within a `Codable` definition to enforce fixed-width encoding for a property:
-```swift
-struct MyStruct: Codable {
-
- /// Always encoded as 4 bytes
- @FixedSize
- var largeInteger: Int32
-}
-```
-
-The `FixedSize` wrapper is available to all `Varint` types: `Int`, `UInt`, `Int32`, `UInt32`, `Int64`, and `UInt64`.
-
-#### Signed integers
-
-Integers are by default [encoded as `Varint` values](BinaryFormat.md#integer-encoding), which is efficient while numbers are small and positive. For numbers which are mostly or also often negative, it is more efficient to store them using `Zig-Zag` encoding. `BinaryCodable` offers the `SignedValue` wrapper that can be applied to `Int`, `Int32` and `Int64` properties to increase the efficiency for negative values.
-
-Whenever your integers are expected to be negative, then you should apply the wrapper:
-```swift
-struct MyStruct: Codable {
-
- /// More efficiently encodes negative numbers
- @SignedValue
- var count: Int
-}
-```
-
-### Enums
-
-Protocol Buffer [enumerations](https://developers.google.com/protocol-buffers/docs/proto3#enum) are supported, with a few notable caveats. Here is the example from the official documentation:
-```proto
-message SearchRequest {
-
- ...
-
- enum Corpus {
- UNIVERSAL = 0;
- WEB = 1;
- IMAGES = 2;
- LOCAL = 3;
- NEWS = 4;
- PRODUCTS = 5;
- VIDEO = 6;
- }
- Corpus corpus = 4;
-}
-```
-The `BinaryCodable` Swift equivalent would be:
-
-```swift
-struct SearchRequest: Codable {
-
- ...
-
- enum Corpus: Int, Codable {
- case universal = 0
- case web = 1
- case images = 2
- case local = 3
- case news = 4
- case products = 5
- case video = 6
- }
-
- var corpus: Corpus
-
- enum CodingKeys: Int, CodingKey {
- case corpus = 4
- }
-}
-```
-
-It should be noted that protobuf enums require a default key `0`.
-
-### Oneof
-
-The protobuf feature [Oneof](https://developers.google.com/protocol-buffers/docs/proto3#oneof) can also be supported using a special enum definition. Given the protobuf definition (from [here](https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md#oneof-fields)):
-
-```proto
-syntax = "proto3";
-message ExampleOneOf {
- int32 field1 = 1;
- oneof alternatives {
- int64 id = 2;
- string name = 3;
- }
-}
-```
-
-The corresponding Swift definition would be:
-
-```swift
-struct ExampleOneOf: Codable {
-
- let field1: Int32
-
- // The oneof field
- let alternatives: Alternatives
-
- // The OneOf definition
- enum Alternatives: Codable, ProtobufOneOf {
- case id(Int64)
- case name(String)
-
- // Field values, must not overlap with `ExampleOneOf.CodingKeys`
- enum CodingKeys: Int, CodingKey {
- case id = 2
- case name = 3
- }
- }
-
- enum CodingKeys: Int, CodingKey {
- case field1 = 1
- // The field id of the Oneof field is not used
- case alternatives = 123456
- }
- }
- ```
-
-Note that the `Alternatives` enum must conform to `ProtobufOneOf`, which changes the encoding to create compatibility with the Protobuf binary format.
-
-**Important** The `ProtobufOneOf` protocol must not be applied to any other types, or encoding/decoding will fail.
diff --git a/README.md b/README.md
index 611b3fa..793e137 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
-This package provides convenient encoding and decoding to/from binary data for all Swift `Codable` types. It also provides limited cross-compatibility to [Google Protocol Buffers](https://developers.google.com/protocol-buffers).
+This package provides convenient encoding and decoding to/from binary data for all Swift `Codable` types.
## Use cases
@@ -22,8 +22,6 @@ One very popular alternative for binary data are Google's [Protocol Buffers](htt
So if you're looking for a decently efficient binary encoder in a pure Swift project, then `BinaryCodable` may be right for you. Simply make your `struct`s (or classes!) conform to `Codable`, and `BinaryCodable` does the rest!
-The [message format](#binary-format) is similar to that of `Protocol Buffers` (with some additions to support more types). It is possible to create [limited compatibility](#protocol-buffer-compatibility) between the two formats to exchange data with systems that don't support Swift.
-
### Alternatives
#### [Protocol Buffers](https://developers.google.com/protocol-buffers)
@@ -45,10 +43,7 @@ Encoding according to the [BSON specification](https://bsonspec.org). Less effic
Simply include in your `Package.swift`:
```swift
dependencies: [
- .package(
- name: "BinaryCodable",
- url: "https://github.com/christophhagen/BinaryCodable",
- from: "1.0.0")
+ .package(url: "https://github.com/christophhagen/BinaryCodable", from: "3.0.0")
],
targets: [
.target(name: "MyTarget", dependencies: [
@@ -110,20 +105,34 @@ Alternatively, the type can be inferred:
let message: Message = try decoder.decode(from: data)
```
-### Limitations
+### Custom encoding and decoding
+
+`BinaryCodable` supports the use of custom encoding and decoding routines by implementing `encode(to:)` and `init(from:)`.
+
+There is only one aspect that's handled differently than the `Codable` documentation specifies, which is the explicit encoding of `nil` in keyed containers.
+Calling `encodeNil(forKey:)` on a keyed container has no effect, there is no explicit `nil` value encoded for the key.
+This results in the `contains()` function during decoding returning `false` for the key.
+This is different to e.g. JSON, where calling `encodeNil(forKey:)` would cause the following encoding:
-`BinaryCodable` supports most features of the `Codable` protocol.
-One known issue is the missing compatibility with `SIMD` types (`import simd`), wich require the `count` property of unkeyed containers.
-This optional property *can* return the number of values in the container.
-Due to the binary format of `BinaryCodable`, this information is not available for unkeyed containers.
-Only once specific types are decoded (e.g. using `decode(Bool.self)`) does the container know how to treat the data.
-So a container with 8 bytes of data could contain 8 `Bool` values, or a single `Double`.
+```json
+{
+ "myProperty" : nil
+}
+```
+
+The implementation of `encodeNil(forKey:)` and `decodeNil(forKey:)` handles this case differently, because the alternatives are not optimal:
+It would be possible to explicitly encode `nil` for a key, but this would cause problems with double optionals in structs (e.g. Int??), which could no longer distinguish between `.some(nil)` and `nil`.
+To fix this issue, an additional `nil` indicator would be needed for **all** values in keyed containers, which would decrease the efficiency of the binary format.
+That doesn't seem reasonable just to support a rarely used feature, since `encodeNil(forKey:)` is never called for automatically synthesized Codable conformances.
+
+The recommendation therefore is to use `encodeIfPresent(_, forKey:)` and `decodeIfPresent(_, forKey:)`.
+Another option would be to use a double optional, since this is basically the information `encodeNil` provides: `nil`, if the key is not present, `.some(nil)`, if the key is present with `nil`, and `value`, if the key is present with a value.
### Errors
-It is possible for both encoding and decoding to fail.
-All possible errors occuring during encoding produce `EncodingError` errors, while unsuccessful decoding produces `DecodingError`s.
-Both are the default Errors provided by Swift, supplied with information describing the nature of the error.
+It's possible for both encoding and decoding to fail.
+Encoding can produce `EncodingError` errors, while unsuccessful decoding produces `DecodingError`s.
+Both are the default Errors provided by Swift, supplied with additional information describing the nature of the error.
See the documentation of the types to learn more about the different error conditions.
#### Handling corrupted data
@@ -132,19 +141,22 @@ The [binary format](BinaryFormat.md) provides no provisions to detect data corru
Additional external measures (checksums, error-correcting codes, ...) should be applied if there is an increased risk of data corruption.
As an example, consider the simple encoding of a `String` inside a `struct`, which consists of a `key` followed by the length of the string in bytes, and the string content.
-The length of the string is encoded using variable-length encoding, so a single bit flip (in the MSB of the length byte) could result in a very large `length` being decoded, causing the decoder to wait for a very large number of bytes to decode the string.
+The length of the string is encoded using variable-length encoding, so a single bit flip (in the MSB of the length byte) could result in a very large `length` being decoded, causing the decoder to wait for a very large number of bytes to decode the string.
This simple error would cause much data to be skipped, potentially corrupting the data stream indefinitely.
At the same time, it is not possible to determine *with certainty* where the error occured, making error recovery difficult without additional information about boundaries between elements.
-The decoding errors provided by the library are therefore only hints about error likely occuring from non-conformance to the binary format or version incompatibility, which are not necessarily the *true* causes of the failures when data corruption is present.
+The decoding errors provided by the library are therefore only hints about errors likely occuring from non-conformance to the binary format or version incompatibility, which are not necessarily the *true* causes of the failures when data corruption is present.
### Coding Keys
-The `Codable` protocol uses [`CodingKey`](https://developer.apple.com/documentation/swift/codingkey) definitions to identify properties of instances. By default, coding keys are generated using the string values of the property names.
+The `Codable` protocol uses [`CodingKey`](https://developer.apple.com/documentation/swift/codingkey) definitions to identify properties of instances.
+By default, coding keys are generated using the string values of the property names.
Similar to JSON encoding, `BinaryCodable` can embed the property names in the encoded data.
-Unlike JSON (which is human-readable), the binary representation produced by `BinaryCodable` is intended for cases when efficient encoding is important. `Codable` allows the use of integer keys for each property, which significantly increases encoding efficiency. You can specify integer keys by adding an `Int` enum conforming to the `CodingKey` protocol to the `Codable` type:
+Unlike JSON (which is human-readable), the binary representation produced by `BinaryCodable` is intended for cases when efficient encoding is important.
+`Codable` allows the use of integer keys for each property, which significantly increases encoding efficiency.
+You can specify integer keys by adding an `Int` enum conforming to the `CodingKey` protocol to the `Codable` type:
```swift
struct Message: Codable {
@@ -165,19 +177,24 @@ struct Message: Codable {
```
The enum must have a raw value of either `Int` or `String`, and the cases must match the property names within the type (it is possible to omit keys for properties which should not be encoded).
-Using integer keys can significantly decrease the binary size, especially for long property names. Additionally, integer keys can be useful when intending to store the binary data persistently. Changes to property names can be performed in the code without breaking the decoding of older data (although this can also be achieved with custom `String` keys).
+Using integer keys can significantly decrease the binary size, especially for long property names.
+Additionally, integer keys can be useful when intending to store the binary data persistently.
+Changes to property names can be performed in the code without breaking the decoding of older data (although this can also be achieved with custom `String` keys).
Notes:
- Small, positive integer keys produce the smallest binary sizes.
- The `0` integer key shouldn't be used, since it is also used internally when encoding `super`.
-- Negative values for integer keys are **not** recommended (but possible). Since the keys are encoded as `Varint`, they are very inefficient for negative numbers.
-- The allowed range for integer keys is from `-576460752303423488` (`-2^59`, inclusive) to `576460752303423487` (`2^59-1`, inclusive). Values outside of these bounds will cause a `fatalError` crash.
+- Negative values for integer keys are **not** rsupported.
+- The allowed range for integer keys is from `0` (inclusive) to `Int64.max` (inclusive).
### Property wrappers
#### Fixed size integers
-While varints are efficient for small numbers, their encoding introduces a storage and computation penalty when the integers are often large, e.g. for random numbers. `BinaryCodable` provides the `FixedSize` wrapper, which forces integers to be encoded using their little-endian binary representations. This means that e.g. an `Int32` is always encoded as 4 byte (instead of 1-5 bytes using Varint encoding). This makes 32-bit `FixedSize` types more efficient than `Varint` if values are often larger than `2^28` (`2^56` for 64-bit types).
+While varints are efficient for small numbers, their encoding introduces a storage and computation penalty when the integers are often large, e.g. for random numbers.
+`BinaryCodable` provides the `FixedSize` wrapper, which forces integers to be encoded using their little-endian binary representations.
+This means that e.g. an `Int32` is always encoded as 4 byte (instead of 1-5 bytes using Varint encoding).
+This makes 32-bit `FixedSize` types more efficient than `Varint` if values are often larger than `2^28` (`2^56` for 64-bit types).
Use the property wrapper within a `Codable` definition to enforce fixed-width encoding for a property:
```swift
@@ -190,31 +207,20 @@ While varints are efficient for small numbers, their encoding introduces a stora
```
The `FixedSize` wrapper is available to all `Varint` types: `Int`, `UInt`, `Int32`, `UInt32`, `Int64`, and `UInt64`.
-
- #### Other property wrappers
-
- There is an additional `SignedValue` wrapper, which is only useful when encoding in [protobuf-compatible format](ProtobufSupport.md#signed-integers).
### Options
#### Sorting keys
-The `BinaryEncoder` provides the `sortKeysDuringEncoding` option, which forces fields in "keyed" containers, such as `struct` properties (and some dictionaries), to be sorted in the binary data. This sorting is done by using either the [integer keys](#coding-keys) (if defined), or the property names. Dictionaries with `Int` or `String` keys are also sorted.
+The `BinaryEncoder` provides the `sortKeysDuringEncoding` option, which forces fields in "keyed" containers, such as `struct` properties (and some dictionaries), to be sorted in the binary data.
+This sorting is done by using either the [integer keys](#coding-keys) (if defined), or the property names.
+Dictionaries with `Int` or `String` keys are also sorted.
-Sorting the binary data does not influence decoding, but introduces a computation penalty during encoding. It should therefore only be used if the binary data must be consistent across multiple invocations.
+Sorting the binary data does not influence decoding, but introduces a computation penalty during encoding.
+It should therefore only be used if the binary data must be consistent across multiple invocations.
-**Note:** The `sortKeysDuringEncoding` option does not *neccessarily* guarantee deterministic binary data, and should be used with care.
-
-#### Encoding optionals in arrays
-
-Sequences of `Optional` values (like arrays, sets, ...) are normally encoded one additional byte to indicate following value (`0x01`) or a `nil` value (`0x00`).
-This works well for all compiler-generated conformances to `Codable`.
-For custom implementations of `func encode(to: Encoder)` and `init(from: Decoder)`, the `encodeNil()` is *not* supported on `UnkeyedEncodingContainer`s.
-If you must use this option, then it's necessary to enable the `prependNilIndexSetForUnkeyedContainers` option on both `BinaryEncoder` and `BinaryDecoder`.
-Optional values are then encoded using a *nil index set*.
-The index of each `nil` element in the sequence is recorded, and only non-nil values are encoded.
-The indices of `nil` elements are then prepended to the data as an array of integers.
-During decoding, this index set is checked to place `nil` values between the non-nil elements at the appropriate indices.
+**Note:** The `sortKeysDuringEncoding` option does **not** guarantee deterministic binary data, and should be used with care.
+Elements of any non-ordered types (Sets, Dictionaries) will appear in random order in the binary data.
### Stream encoding and decoding
@@ -271,17 +277,57 @@ try decoder.read { element in
There is also the possibility to read all elements at once using `readAll()`, or to read only one element at a time (`readElement()`).
-### Protocol Buffer compatibility
-
-Achieving Protocol Buffer compatibility is described in [ProtobufSupport.md](ProtobufSupport.md).
-
## Binary format
To learn more about the encoding format, see [BinaryFormat.md](BinaryFormat.md).
+## Legacy versions and migration
+
+Version 3 of `BinaryCodable` has significantly changed the binary format, which means that the two versions are **not** cross-compatible.
+The [format](BinaryFormat.md) was changed to provide support for all `Codable` features, which was not possible with the previous format, that was adapted from [Protocol Buffers](https://developers.google.com/protocol-buffers).
+The redesign of the library also reduced code complexity and size, which lead to some speed improvements and greater reliability.
+
+The support for interoperability with [Protocol Buffers](https://developers.google.com/protocol-buffers) was also dropped, since the binary formats are no longer similar.
+The functionality was extracted to a separate library called [ProtobufCodable](https://github.com/christophhagen/ProtobufCodable).
+
+### Migrating from 2.x to 3.0
+
+To convert data from the [legacy format](LegacyFormat) to the new version, the data has to be decoded with version 2 and re-encoded with version 3.
+The Swift Package Manager currently doesn't allow to include the same dependency twice (with different versions), so the legacy version has been stripped down to the essentials and is provided as the stand-alone package [LegacyBinaryCodable](https://christophhagen.de/LegacyBinaryCodable).
+It only allows decoding, and can be integrated as a separate dependency:
+
+```swift
+dependencies: [
+ .package(url: "https://github.com/christophhagen/BinaryCodable", from: "3.0.0"),
+ .package(url: "https://github.com/christophhagen/LegacyBinaryCodable", from: "2.0.0"),
+
+],
+targets: [
+ .target(name: "MyTarget", dependencies: [
+ .product(name: "BinaryCodable", package: "BinaryCodable"),
+ .product(name: "LegacyBinaryCodable", package: "LegacyBinaryCodable")
+ ])
+]
+```
+
+In the code, you can then decode and re-encode:
+
+```swift
+import BinaryCodable
+import LegacyBinaryCodable
+
+func reencode(data: Data, as type: T.Type) throws -> Data where T: Codable {
+ let decoder = LegacyBinaryDecoder()
+ let value = try decoder.decode(T.self, from data: Data)
+ let encoder = BinaryEncoder()
+ return try encoder.encode(value)
+}
+```
+
## Tests
-The library comes with an extensive test suite, which checks that encoding works correctly for many cases. These tests can be executed using ```swift test``` from the package root, or when opening the package using Xcode.
+The library comes with an extensive test suite, which checks that encoding works correctly for many cases.
+These tests can be executed using ```swift test``` from the package root, or when opening the package using Xcode.
## License
@@ -289,12 +335,15 @@ MIT. See [License.md](License.md)
## Roadmap
-### Generate protobuf definitions
+### Additional tests
-It should be possible to generate a string containing a working Protobuf definition for any type that is determined to be Protobuf compatible.
+While the test suite covers many cases, there is no complete code coverage.
+Especially the bahaviour in error conditions can use additional testing to ensure that there are no edge cases where the program crashes, or does some other weird thing.
### Speed
+One option could be to use a common data storage during encoding and decoding, so that the individual containers can be converted to `struct`s to make them more lightweight. It may also be possible to prevent some unnecessary data copying.
+
Increasing the speed of the encoding and decoding process is not a huge priority at the moment.
If you have any pointers on how to improve the performance further, feel free to contribute.
diff --git a/Sources/BinaryCodable/BinaryDecoder.swift b/Sources/BinaryCodable/BinaryDecoder.swift
index 49a6a3c..ecac131 100644
--- a/Sources/BinaryCodable/BinaryDecoder.swift
+++ b/Sources/BinaryCodable/BinaryDecoder.swift
@@ -1,7 +1,7 @@
import Foundation
/**
- An encoder to convert binary data back to `Codable` objects.
+ A decoder to convert data encoded with `BinaryEncoder` back to a `Codable` types.
To decode from data, instantiate a decoder and specify the type:
```
@@ -18,44 +18,20 @@ import Foundation
```
let message = try BinaryDecoder.decode(Message.self, from: data)
```
- - Note: A single decoder can be used to decode multiple messages.
+ - Note: A single decoder can be used to decode multiple objects.
*/
-public final class BinaryDecoder {
+public struct BinaryDecoder {
/**
Any contextual information set by the user for decoding.
This dictionary is passed to all containers during the decoding process.
*/
- public var userInfo = [CodingUserInfoKey : Any]()
+ public var userInfo: [CodingUserInfoKey : Any] = [:]
/**
- Assumes that unkeyed containers are encoded using a set of indices for `nil` values.
-
- Refer to the ``prependNilIndexSetForUnkeyedContainers`` property of ``BinaryEncoder``
- for more information about the binary data format in both cases.
-
- - Note: This option defaults to `false`
- - Note: To decode successfully, the encoder must use the same setting for `prependNilIndexSetForUnkeyedContainers`.
- */
- public var containsNilIndexSetForUnkeyedContainers: Bool = false
-
- /**
- The info for decoding.
-
- Combines the info data provided by the user with the internal keys of the decoding options.
- */
- private var fullInfo: [CodingUserInfoKey : Any] {
- var info = userInfo
- if containsNilIndexSetForUnkeyedContainers {
- info[CodingOption.prependNilIndicesForUnkeyedContainers.infoKey] = true
- }
- return info
- }
-
- /**
- Create a new binary encoder.
- - Note: A single decoder can be used to decode multiple messages.
+ Create a new decoder.
+ - Note: A single decoder can be reused to decode multiple objects.
*/
public init() {
@@ -69,21 +45,12 @@ public final class BinaryDecoder {
- Throws: Errors of type `DecodingError`
*/
public func decode(_ type: T.Type = T.self, from data: Data) throws -> T where T: Decodable {
- let root = DecodingNode(data: data, path: [], info: fullInfo)
- return try type.init(from: root)
- }
-
- /**
- Decode a type from a data stream.
-
- This function is the pendant to `encodeForStream()` on ``BinaryEncoder``, and decodes a type from a data stream.
- The additional length information added to the stream is used to correctly decode each element.
- - Note: This function is not exposed publicly to keep the API easy to understand.
- Advanced features like stream decoding are handled by ``BinaryStreamDecoder``.
- */
- func decode(_ type: T.Type = T.self, fromStream provider: BinaryStreamProvider) throws -> T where T: Decodable {
- let root = DecodingNode(decoder: provider, path: [], info: fullInfo)
- return try type.init(from: root)
+ // Directly decode primitives, otherwise it would be decoded with a nil indicator
+ if let BaseType = T.self as? DecodablePrimitive.Type {
+ return try BaseType.init(data: data, codingPath: []) as! T
+ }
+ let node = try DecodingNode(data: data, parentDecodedNil: false, codingPath: [], userInfo: userInfo)
+ return try T.init(from: node)
}
/**
diff --git a/Sources/BinaryCodable/BinaryEncoder.swift b/Sources/BinaryCodable/BinaryEncoder.swift
index 75af383..7b1e112 100644
--- a/Sources/BinaryCodable/BinaryEncoder.swift
+++ b/Sources/BinaryCodable/BinaryEncoder.swift
@@ -14,12 +14,15 @@ import Foundation
- Note: An ecoder can be used to encode multiple messages.
*/
-public final class BinaryEncoder {
+public struct BinaryEncoder {
+
+ /// The user info key for the ``sortKeysDuringEncoding`` option.
+ public static let userInfoSortKey: CodingUserInfoKey = .init(rawValue: "sortByKey")!
/**
Sort keyed data in the binary representation.
- Enabling this option causes all keyed data (e.g. `Dictionary`, `Struct`) to be sorted by their keys before encoding.
+ Enabling this option causes all data in keyed containers (e.g. `Dictionary`, `Struct`) to be sorted by their keys before encoding.
This option can enable deterministic encoding where the binary output is consistent across multiple invocations.
- Warning: Output will not be deterministic when using `Set`, or `Dictionary` where `Key` is not `String` or `Int`.
@@ -28,82 +31,66 @@ public final class BinaryEncoder {
This option has no impact on decoding using `BinaryDecoder`.
- Enabling this option will add the `CodingUserInfoKey(rawValue: "sort")` to the `userInfo` dictionary.
+ Enabling this option will add the `CodingUserInfoKey(rawValue: "sortByKey")` to the `userInfo` dictionary.
+ This key is also available as ``userInfoSortKey``
- Note: The default value for this option is `false`.
*/
- public var sortKeysDuringEncoding: Bool = false
-
- /**
- Add a set of indices for `nil` values in unkeyed containers.
-
- This option is only necessary for custom implementations of `func encode(to:)`, when using `encodeNil()` on `UnkeyedEncodingContainer`.
-
- If this option is set to `true`, then the encoded binary data first contains a list of indexes for each position where `nil` is encoded.
- After this data the remaining (non-nil) values are added.
- If this option is `false`, then each value is prepended with a byte `1` for non-nil values, and a byte `0` for `nil` values.
-
- An index set is encoded using first the number of elements, and then each element, all encoded as var-ints.
-
- One benefit of this option is that top-level sequences can be joined using their binary data, where `encoded([a,b]) | encoded([c,d]) == encoded([a,b,c,d])`.
-
- - Note: This option defaults to `false`
- - Note: To decode successfully, the decoder must use the same setting for `containsNilIndexSetForUnkeyedContainers`.
- - Note: Using `encodeNil()` on `UnkeyedEncodingContainer` without this option results in a fatal error.
- */
- public var prependNilIndexSetForUnkeyedContainers: Bool = false
-
- /**
- Any contextual information set by the user for encoding.
-
- This dictionary is passed to all containers during the encoding process.
-
- Contains also keys for any custom options set for the encoder.
- See `sortKeysDuringEncoding`.
- */
- public var userInfo = [CodingUserInfoKey : Any]()
-
- /**
- The info for encoding.
-
- Combines the info data provided by the user with the internal keys of the encoding options.
- */
- private var fullInfo: [CodingUserInfoKey : Any] {
- var info = userInfo
- if sortKeysDuringEncoding {
- info[CodingOption.sortKeys.infoKey] = true
+ public var sortKeysDuringEncoding: Bool {
+ get {
+ userInfo[BinaryEncoder.userInfoSortKey] as? Bool ?? false
}
- if prependNilIndexSetForUnkeyedContainers {
- info[CodingOption.prependNilIndicesForUnkeyedContainers.infoKey] = true
+ set {
+ userInfo[BinaryEncoder.userInfoSortKey] = newValue
}
- return info
}
-
+
+ /// Any contextual information set by the user for encoding.
+ public var userInfo: [CodingUserInfoKey : Any] = [:]
+
/**
- Create a new binary encoder.
+ Create a new encoder.
- Note: An encoder can be used to encode multiple messages.
*/
public init() {
-
+
}
-
+
/**
Encode a value to binary data.
- Parameter value: The value to encode
- Returns: The encoded data
- Throws: Errors of type `EncodingError`
*/
- public func encode(_ value: Encodable) throws -> Data {
- let isOptional = value is AnyOptional
- let root = EncodingNode(path: [], info: fullInfo, optional: isOptional)
- try value.encode(to: root)
- return root.data
+ public func encode(_ value: T) throws -> Data where T: Encodable {
+ // Directly encode primitives, otherwise:
+ // - Data would be encoded as an unkeyed container
+ // - There would always be a nil indicator byte 0x00 at the beginning
+ // NOTE: The comparison of the types is necessary, since otherwise optionals are matched as well.
+ if T.self is EncodablePrimitive.Type, let value = value as? EncodablePrimitive {
+ return value.encodedData
+ }
+ let encoder = EncodingNode(needsLengthData: false, codingPath: [], userInfo: userInfo)
+ try value.encode(to: encoder)
+ return try encoder.completeData()
+ }
+
+ /**
+ Encode a single value to binary data using a default encoder.
+ - Parameter value: The value to encode
+ - Returns: The encoded data
+ - Throws: Errors of type `EncodingError`
+ */
+ public static func encode(_ value: Encodable) throws -> Data {
+ try BinaryEncoder().encode(value)
}
+ // MARK: Stream encoding
+
/**
Encodes a value to binary data for use in a data stream.
- This function differs from 'normal' encoding since additional length information is embedded into the binary data.
+ This function differs from 'normal' encoding by the additional length information prepended to the element..
This information is used when decoding values from a data stream.
- Note: This function is not exposed publicly to keep the API easy to understand.
@@ -115,19 +102,8 @@ public final class BinaryEncoder {
- Throws: Errors of type `EncodingError`
*/
func encodeForStream(_ value: Encodable) throws -> Data {
- let isOptional = value is AnyOptional
- let root = EncodingNode(path: [], info: fullInfo, optional: isOptional)
- try value.encode(to: root)
- return root.dataWithLengthInformationIfRequired
- }
-
- /**
- Encode a single value to binary data using a default encoder.
- - Parameter value: The value to encode
- - Returns: The encoded data
- - Throws: Errors of type `EncodingError`
- */
- public static func encode(_ value: Encodable) throws -> Data {
- try BinaryEncoder().encode(value)
+ let encoder = EncodingNode(needsLengthData: true, codingPath: [], userInfo: userInfo)
+ try value.encode(to: encoder)
+ return try encoder.completeData()
}
}
diff --git a/Sources/BinaryCodable/BinaryFileDecoder.swift b/Sources/BinaryCodable/BinaryFileDecoder.swift
index b900ac0..24f6526 100644
--- a/Sources/BinaryCodable/BinaryFileDecoder.swift
+++ b/Sources/BinaryCodable/BinaryFileDecoder.swift
@@ -31,11 +31,11 @@ import Foundation
*/
public final class BinaryFileDecoder where Element: Decodable {
- private let handle: FileHandle
+ private let file: FileHandle
private let decoder: BinaryDecoder
- private let buffer: BinaryStreamBuffer
+ private let endIndex: UInt64
/**
Create a file decoder.
@@ -47,10 +47,16 @@ public final class BinaryFileDecoder where Element: Decodable {
- Throws: An error, if the file handle could not be created.
*/
public init(fileAt url: URL, decoder: BinaryDecoder = .init()) throws {
- let handle = try FileHandle(forReadingFrom: url)
- self.handle = handle
+ let file = try FileHandle(forReadingFrom: url)
+ self.file = file
self.decoder = decoder
- self.buffer = BinaryStreamBuffer(data: handle)
+ if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) {
+ self.endIndex = try file.seekToEnd()
+ try file.seek(toOffset: 0)
+ } else {
+ self.endIndex = file.seekToEndOfFile()
+ file.seek(toFileOffset: 0)
+ }
}
deinit {
@@ -66,9 +72,9 @@ public final class BinaryFileDecoder where Element: Decodable {
*/
public func close() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.4, watchOS 6.2, *) {
- try handle.close()
+ try file.close()
} else {
- handle.closeFile()
+ file.closeFile()
}
}
@@ -78,7 +84,7 @@ public final class BinaryFileDecoder where Element: Decodable {
Read all elements in the file, and handle each element using a closure.
- Parameter elementHandler: The closure to handle each element as it is decoded.
- - Throws: Decoding errors of type ``DecodingError``.
+ - Throws: Decoding errors of type `DecodingError`.
*/
public func read(_ elementHandler: (Element) throws -> Void) throws {
while let element = try readElement() {
@@ -89,7 +95,7 @@ public final class BinaryFileDecoder where Element: Decodable {
/**
Read all elements at once.
- Returns: The elements decoded from the file.
- - Throws: Errors of type ``DecodingError``
+ - Throws: Errors of type `DecodingError`
*/
public func readAll() throws -> [Element] {
var result = [Element]()
@@ -117,77 +123,54 @@ public final class BinaryFileDecoder where Element: Decodable {
/**
Read a single elements from the current position in the file.
- Returns: The element decoded from the file, or `nil`, if no more data is available.
- - Throws: Errors of type ``DecodingError``
+ - Throws: Errors of type `DecodingError`
*/
public func readElement() throws -> Element? {
- guard buffer.hasMoreBytes else {
+ guard !isAtEnd else {
return nil
}
- do {
- let element = try decodeNextElement()
- // Remove the buffered data since element was correctly decoded
- buffer.discardUsedBufferData()
- return element
- } catch {
- buffer.discardUsedBufferData()
- throw error
- }
+ // Read length/nil indicator
+ let data = try decodeNextDataOrNilElement()
+ let node = try DecodingNode(data: data, parentDecodedNil: true, codingPath: [], userInfo: decoder.userInfo)
+ return try Element.init(from: node)
}
+}
- private func decodeNextElement() throws -> Element {
- if Element.self is AnyOptional.Type {
- return try decodeOptional()
- } else {
- return try decodeNonOptional()
+extension BinaryFileDecoder: DecodingDataProvider {
+
+ var codingPath: [any CodingKey] { [] }
+
+ private var currentOffset: UInt64 {
+ if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) {
+ return (try? file.offset()) ?? endIndex
}
+ return file.offsetInFile
}
- private func decodeNonOptional() throws -> Element {
- return try decoder.decode(fromStream: buffer)
+ var isAtEnd: Bool {
+ currentOffset >= endIndex
}
- private func decodeOptional() throws -> Element {
- guard try buffer.getByte(path: []) > 0 else {
- return try decoder.decode(from: Data())
- }
- return try decodeNonOptional()
+ func nextByte() throws -> UInt64 {
+ let byte = try getBytes(1).first!
+ return UInt64(byte)
}
-}
-extension FileHandle: BinaryStreamProvider {
+ var numberOfRemainingBytes: Int {
+ Int(endIndex - currentOffset)
+ }
- func getBytes(_ count: Int, path: [CodingKey]) throws -> Data {
+ func getBytes(_ count: Int) throws -> Data {
guard #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) else {
- let data = readData(ofLength: count)
+ let data = file.readData(ofLength: count)
guard data.count == count else {
- throw DecodingError.prematureEndOfData(path)
+ throw DecodingError.prematureEndOfData([])
}
return data
}
- guard let data = try read(upToCount: count) else {
- throw DecodingError.prematureEndOfData(path)
+ guard let data = try file.read(upToCount: count) else {
+ throw DecodingError.prematureEndOfData([])
}
return data
}
-
- var hasMoreBytes: Bool {
- if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *) {
- return moreBytesAvailable
- }
-
- if readData(ofLength: 1).first != nil {
- seek(toFileOffset: offsetInFile - 1)
- return true
- }
- return false
- }
-
- @available(macOS 10.15.4, iOS 13.4, tvOS 13.4, watchOS 6.2, *)
- private var moreBytesAvailable: Bool {
- if (try? read(upToCount: 1)?.first) != nil {
- try? seek(toOffset: offset() - 1)
- return true
- }
- return false
- }
}
diff --git a/Sources/BinaryCodable/BinaryFileEncoder.swift b/Sources/BinaryCodable/BinaryFileEncoder.swift
index ee8b395..f9a3cc4 100644
--- a/Sources/BinaryCodable/BinaryFileEncoder.swift
+++ b/Sources/BinaryCodable/BinaryFileEncoder.swift
@@ -56,7 +56,7 @@ public final class BinaryFileEncoder where Element: Encodable {
Write a single element to the file.
- Note: This function will throw an error or exception if the file handle has already been closed.
- Parameter element: The element to encode.
- - Throws: Errors of type ``EncodingError``
+ - Throws: Errors of type `EncodingError`
*/
public func write(_ element: Element) throws {
let data = try stream.encode(element)
@@ -71,9 +71,9 @@ public final class BinaryFileEncoder where Element: Encodable {
Write a sequence of elements to the file.
This is a convenience function calling `write(_ element:)` for each element of the sequence in order.
-
+
- Parameter sequence: The sequence to encode
- - Throws: Errors of type ``EncodingError``
+ - Throws: Errors of type `EncodingError`
*/
public func write(contentsOf sequence: S) throws where S: Sequence, S.Element == Element {
try sequence.forEach(write)
diff --git a/Sources/BinaryCodable/BinaryStreamDecoder.swift b/Sources/BinaryCodable/BinaryStreamDecoder.swift
index 71ad31e..e60c406 100644
--- a/Sources/BinaryCodable/BinaryStreamDecoder.swift
+++ b/Sources/BinaryCodable/BinaryStreamDecoder.swift
@@ -17,7 +17,23 @@ public final class BinaryStreamDecoder where Element: Decodable {
*/
private let decoder: BinaryDecoder
- private let buffer: DataDecodingBuffer
+ /**
+ The buffer storing the data while elements are being decoded.
+ */
+ private var buffer = Data()
+
+ /**
+ The length of the next element.
+
+ This property is `nil`, while the length is being decoded.
+ */
+ private var lengthOfCurrentElement: Int?
+
+ /// The current length of the next element, while decoding the bytes
+ private var currentLength: UInt64 = 0
+
+ /// The number of length bytes already decoded
+ private var numberOfLengthBytes = 0
/**
Create a stream decoder.
@@ -26,26 +42,40 @@ public final class BinaryStreamDecoder where Element: Decodable {
*/
public init(decoder: BinaryDecoder = .init()) {
self.decoder = decoder
- self.buffer = DataDecodingBuffer()
}
-
-
+
+ // MARK: Buffer operations
+
+ private var isAtEnd: Bool {
+ buffer.isEmpty
+ }
+
+ private func nextByte() -> UInt8? {
+ buffer.popFirst()
+ }
+
+ private func nextBytes(_ count: Int) -> Data {
+ let data = buffer[buffer.startIndex.. where Element: Decodable {
*/
@discardableResult
public func discardBytes(_ count: Int) -> Int {
- buffer.discard(bytes: count)
+ let oldCount = buffer.count
+ buffer = buffer.dropFirst(count)
+ return oldCount - buffer.count
}
// MARK: Decoding
- /**
- Read elements from the stream until no more bytes are available.
-
- - Parameter returnElementsBeforeError: If set to `true`,
- then all successfully decoded elements will be returned if an error occurs. Defaults to `false`
- - Throws: Decoding errors of type ``DecodingError``.
- - Note: This function is deprecated. If you want to decode elements, use ``decodeElements()``, or use ``decodeElementsUntilError()``, if you expect errors in the data stream.
- */
- @available(*, deprecated, message: "Use add() and decodeElementsUntilError() or decodeElements() instead.")
- public func decode(_ data: Data, returnElementsBeforeError: Bool = false) throws -> [Element] {
- buffer.addToBuffer(data)
-
- var results = [Element]()
- while buffer.hasMoreBytes {
- do {
- guard let element = try decodeElement() else {
- return results
- }
- results.append(element)
- } catch {
- if returnElementsBeforeError {
- return results
- }
- throw error
- }
- }
- return results
- }
-
/**
Read elements from the stream until an error occurs.
-
+
This function may be useful if the data is corrupted. The readable elements can be decoded until a decoding error is encountered.
- Returns: The elements that could be decoded until an error occured or the buffer was insufficient, and the decoding error, if one occured.
*/
public func decodeElementsUntilError() -> (elements: [Element], error: Error?) {
var results = [Element]()
- while buffer.hasMoreBytes {
+ while !buffer.isEmpty {
do {
guard let element = try decodeElement() else {
return (results, nil)
@@ -108,18 +111,18 @@ public final class BinaryStreamDecoder where Element: Decodable {
}
return (results, nil)
}
-
+
/**
Read elements until no more data is available.
-
+
- Note: If a decoding error occurs within the data, then the elements successfully decoded before are lost.
If you expect decoding errors to occur, either decode elements individually using ``decodeElement()``, or use ``decodeElementsUntilError()``.
- Returns: The elements that could be decoded with the available data.
- - Throws: Decoding errors of type ``DecodingError``.
+ - Throws: Decoding errors of type `DecodingError`.
*/
public func decodeElements() throws -> [Element] {
var results = [Element]()
- while buffer.hasMoreBytes {
+ while !isAtEnd {
guard let element = try decodeElement() else {
return results
}
@@ -130,27 +133,83 @@ public final class BinaryStreamDecoder where Element: Decodable {
/**
Attempt to decode a single element from the stream.
-
+
If insufficient bytes are available, then no element is returned, and the internal buffer is kept intact.
- Returns: The decoded element, if enough bytes are available.
- - Throws: Decoding errors of type ``DecodingError``.
+ - Throws: Decoding errors of type `DecodingError`.
*/
public func decodeElement() throws -> Element? {
- do {
- let element: Element = try decoder.decode(fromStream: buffer)
- // Remove the buffered data since element was correctly decoded
- buffer.discardUsedBufferData()
- return element
- } catch {
- if buffer.didExceedBuffer {
- // Insufficient data for now, reuse buffered bytes next time
- buffer.resetToBeginOfBuffer()
- return nil
- } else {
- // Remove the buffered data since element was correctly decoded
- buffer.discardUsedBufferData()
- throw error
+ if let length = lengthOfCurrentElement {
+ return try decodeElement(length: length)
+ }
+
+ // Start by checking the nil bit and decode the first byte of the length
+ if numberOfLengthBytes == 0 {
+ guard let first = nextByte() else {
+ return nil // Wait for first byte
+ }
+
+ // Check the nil indicator bit
+ guard first & 0x01 == 0 else {
+ // Decode a nil element (throws an error if the element can't be nil)
+ let node = try DecodingNode(data: nil, parentDecodedNil: true, codingPath: [], userInfo: decoder.userInfo)
+ return try Element.init(from: node)
+ // No need to reset any state
+ }
+
+ // Ensure that the decoding of the first byte is skipped until a full element is decoded
+ numberOfLengthBytes = 1
+
+ // Check if more length bytes are needed
+ if first & 0x80 == 0 {
+ // Finished with the length
+ return try decodeElement(length: Int(first >> 1))
}
+ // Start the length decoding
+ currentLength = UInt64(first)
+ }
+
+ // Continue decoding the length
+ while numberOfLengthBytes < 8 {
+ guard let nextByte = nextByte() else {
+ return nil // Wait for more bytes
+ }
+ // Insert the last 7 bit of the byte at the end
+ currentLength += UInt64(nextByte & 0x7F) << (numberOfLengthBytes * 7)
+ numberOfLengthBytes += 1
+ // Check if an additional byte is coming
+ guard nextByte & 0x80 > 0 else {
+ // Finished decoding the length
+ return try decodeElement(length: Int(currentLength >> 1))
+ }
+ }
+ // Decode the last byte
+ guard let nextByte = nextByte() else {
+ return nil // Wait for more bytes
}
+ // The 9th byte has no next-byte bit, so all 8 bits are used
+ currentLength += UInt64(nextByte) << 56
+ return try decodeElement(length: Int(currentLength >> 1))
+ }
+
+ private func decodeElement(length: Int) throws -> Element? {
+ guard numberOfBytesInBuffer >= length else {
+ // Wait for enough bytes to decode the next element
+ self.lengthOfCurrentElement = length
+ return nil
+ }
+ let data = nextBytes(length)
+
+ // Reset the state to begin with length/nil decoding
+ self.lengthOfCurrentElement = nil
+ self.currentLength = 0
+ self.numberOfLengthBytes = 0
+
+ let node = try DecodingNode(data: data, parentDecodedNil: true, codingPath: [], userInfo: decoder.userInfo)
+ return try Element.init(from: node)
+ }
+
+ private func corrupted(_ message: String) -> DecodingError {
+ return .corrupted(message, codingPath: [])
}
}
diff --git a/Sources/BinaryCodable/BinaryStreamEncoder.swift b/Sources/BinaryCodable/BinaryStreamEncoder.swift
index 55f8c1f..c588969 100644
--- a/Sources/BinaryCodable/BinaryStreamEncoder.swift
+++ b/Sources/BinaryCodable/BinaryStreamEncoder.swift
@@ -62,7 +62,7 @@ public final class BinaryStreamEncoder where Element: Encodable {
Decoding using a simple ``BinaryDecoder`` will not be successful.
- Parameter element: The element to encode.
- Returns: The next chunk of the encoded binary stream.
- - Throws: Errors of type ``EncodingError``
+ - Throws: Errors of type `EncodingError`
*/
public func encode(_ element: Element) throws -> Data {
try encoder.encodeForStream(element)
@@ -74,7 +74,7 @@ public final class BinaryStreamEncoder where Element: Encodable {
This function performs multiple calls to ``encode(_:)`` to convert all elements of the sequence, and then returns the joined data.
- Parameter sequence: The sequence of elements to encode
- Returns: The binary data of the encoded sequence elements
- - Throws: Errors of type ``EncodingError``
+ - Throws: Errors of type `EncodingError`
*/
public func encode(contentsOf sequence: S) throws -> Data where S: Sequence, S.Element == Element {
try sequence.map(encode).joinedData
diff --git a/Sources/BinaryCodable/BinaryStreamProvider.swift b/Sources/BinaryCodable/BinaryStreamProvider.swift
deleted file mode 100644
index 689cebc..0000000
--- a/Sources/BinaryCodable/BinaryStreamProvider.swift
+++ /dev/null
@@ -1,91 +0,0 @@
-import Foundation
-
-/**
- A protocol to provide continuous data for decoding.
-
- This protocol can be used in conjuction with ``BinaryStreamDecoder`` to decode longer data streams of sequential elements.
- This can be helpful when either the data is not immediatelly available all at once (e.g. when receiving data over a network)
- or when the data is too large to keep it in memory (e.g. when reading a large file).
-
- Implement this protocol according to the data source for encoded data, and then pass it to a ``BinaryStreamDecoder`` to decode individual elements.
- - Note: Successful decoding is only possible if the data stream is also encoded using a ``BinaryStreamEncoder``.
- */
-protocol BinaryStreamProvider {
-
- /**
- A callback to the data source to get the next chuck of data as required by the current decoding step.
-
- The data should be provided in a best-effort manner, if they are available.
- If insufficient bytes are available, e.g. if network data is still in transit,
- then a ``DecodingError`` of type `dataCorrupted` should be thrown.
- This signals to the decoder that not all data is available,
- so that decoding can continue once more data is available.
- - Note: There is no need to buffer incoming data until an element is successfully decoded.
- Any bytes passed to the decoder as the result of this function are internally buffered and can be discarded.
-
- - Parameter count: The number of bytes to provide as the result.
- - Returns: The next `count` bytes in the data stream.
- */
- func getBytes(_ count: Int, path: [CodingKey]) throws -> Data
-
- /**
- Indicate if there are any bytes to read for the decoder.
-
- If the data stream has at least one byte available, then this function should return `true`.
- The decoder uses this function to check if an attempt should be made to decode more elements.
-
- - Note: There is no need to buffer incoming data until an element is successfully decoded.
- It is safe to return `true`, even if not enough bytes are available to decode a full element.
-
- - Returns: `true`, if bytes are available for decoding.
- */
- var hasMoreBytes: Bool { get }
-}
-
-extension BinaryStreamProvider {
-
- func getByte(path: [CodingKey]) throws -> UInt8 {
- let data = try getBytes(1, path: path)
- return data[data.startIndex]
- }
-
- func getDataOfVarint(path: [CodingKey]) throws -> Data {
- var result = [UInt8]()
- for _ in 0...7 {
- let byte = try getByte(path: path)
- result.append(byte)
- if byte & 0x80 == 0 {
- return Data(result)
- }
- }
- let byte = try getByte(path: path)
- result.append(byte)
- return Data(result)
- }
-
- func getVarint(path: [CodingKey]) throws -> Int {
- let data = try getDataOfVarint(path: path)
- return try .init(fromVarint: data, path: path)
- }
-
- func getData(for dataType: DataType, path: [CodingKey]) throws -> Data {
- switch dataType {
- case .variableLengthInteger:
- return try getDataOfVarint(path: path)
- case .byte:
- return try getBytes(1, path: path)
- case .twoBytes:
- return try getBytes(2, path: path)
- case .variableLength:
- let count = try getVarint(path: path)
- guard count >= 0 else {
- throw DecodingError.invalidDataSize(path)
- }
- return try getBytes(count, path: path)
- case .fourBytes:
- return try getBytes(4, path: path)
- case .eightBytes:
- return try getBytes(8, path: path)
- }
- }
-}
diff --git a/Sources/BinaryCodable/Common/AbstractNode.swift b/Sources/BinaryCodable/Common/AbstractNode.swift
index ea88c43..98883f9 100644
--- a/Sources/BinaryCodable/Common/AbstractNode.swift
+++ b/Sources/BinaryCodable/Common/AbstractNode.swift
@@ -25,33 +25,14 @@ class AbstractNode {
*/
let userInfo: UserInfo
-
- /**
- Add a set of indices for `nil` values in unkeyed containers.
-
- This option changes the encoding of unkeyed sequences like arrays with optional values.
-
- If this option is set to `true`, then the encoded binary data first contains a list of indexes for each position where `nil` is encoded.
- After this data the remaining (non-nil) values are added.
- If this option is `false`, then each value is prepended with a byte `1` for non-nil values, and a byte `0` for `nil` values.
-
- Using an index set is generally more efficient, expect for large sequences with many `nil` values.
- An index set is encoded using first the number of elements, and then each element, all encoded as var-ints.
-
- - Note: This option defaults to `true`
- - Note: To decode successfully, the decoder must use the same setting for `containsNilIndexSetForUnkeyedContainers`.
- */
- var prependNilIndexSetForUnkeyedContainers: Bool {
- userInfo.has(.prependNilIndicesForUnkeyedContainers)
- }
-
/**
Create an abstract node.
- - Parameter path: The path to get to this point in encoding or decoding
- - Parameter info: Contextual information set by the user
+ - Parameter codingPath: The path to get to this point in encoding or decoding
+ - Parameter userInfo: Contextual information set by the user
*/
- init(path: [CodingKey], info: UserInfo) {
- self.codingPath = path
- self.userInfo = info
+ init(codingPath: [CodingKey], userInfo: UserInfo) {
+ self.codingPath = codingPath
+ self.userInfo = userInfo
}
+
}
diff --git a/Sources/BinaryCodable/Common/AnyCodingKey.swift b/Sources/BinaryCodable/Common/AnyCodingKey.swift
deleted file mode 100644
index 7912907..0000000
--- a/Sources/BinaryCodable/Common/AnyCodingKey.swift
+++ /dev/null
@@ -1,35 +0,0 @@
-
-/// Helpful for appending mixed coding keys.
-struct AnyCodingKey: CodingKey {
- var stringValue: String
- var intValue: Int?
-
- init(intValue: Int) {
- self.intValue = intValue
- self.stringValue = "\(intValue)"
- }
-
- init(stringValue: String) {
- self.intValue = nil
- self.stringValue = stringValue
- }
-}
-
-extension AnyCodingKey {
- init(_ key: T) {
- self.stringValue = key.stringValue
- self.intValue = key.intValue
- }
-}
-
-extension AnyCodingKey: ExpressibleByStringLiteral {
- init(stringLiteral value: String) {
- self.init(stringValue: value)
- }
-}
-
-extension AnyCodingKey: ExpressibleByIntegerLiteral {
- init(integerLiteral value: Int) {
- self.init(intValue: value)
- }
-}
diff --git a/Sources/BinaryCodable/Common/DataType.swift b/Sources/BinaryCodable/Common/DataType.swift
deleted file mode 100644
index e08aaeb..0000000
--- a/Sources/BinaryCodable/Common/DataType.swift
+++ /dev/null
@@ -1,96 +0,0 @@
-import Foundation
-
-/**
- The data type specifying how a value is encoded on the wire.
-
- The data type is mixed into the key for each value to indicate how many bytes the value occupies in the following bytes.
-
- The data type is equivalent to the [Protocol Buffer Wire Type](https://developers.google.com/protocol-buffers/docs/encoding#structure), but extended to support more data types.
- */
-public enum DataType: Int {
-
- /**
- An integer value encoded as a Base128 Varint.
- The length can be determined by reading the value,
- where the first (MSB) indicates whether another byte follows.
- A Varint consumes up to 9 bytes, where all bits from the last byte are used,
- resulting in `8 * 7 + 8 = 64` usable bits.
-
- For Protobuf encoding, the Varint can use up to 10 bytes, since the MSB is used for all bytes, including byte 9.
-
- See also [Protocol Buffers: Base 128 Varints](https://developers.google.com/protocol-buffers/docs/encoding#varints).
-
- Used for: `Int`, `Int32`, `Int64`, `UInt`, `UInt32`, `UInt64`, `Bool`
- */
- case variableLengthInteger = 0
-
- /**
- The value is encoded as a single byte.
-
- - Note: This data type is incompatible with the protocol buffer specification.
-
- Used for: `UInt8`, `Int8`
- */
- case byte = 6
-
- /**
- The value is encoded as two bytes.
-
- - Note: This data type is incompatible with the protocol buffer specification.
-
- Used for: `Int16`, `UInt16`
- */
- case twoBytes = 7
-
- /**
- The value is encoded using first a length (as a UInt64 var-int) followed by the bytes.
-
- Used by: `String`, `Data`, complex types
- */
- case variableLength = 2
-
- /**
- The value is encoded using four bytes.
-
- Used for: `Float`, `FixedWidth`, `FixedWidth`
- */
- case fourBytes = 5
-
- /**
- The value is encoded using eight bytes.
-
- Used by: `Double`, `FixedWidth`, `FixedWidth`, `FixedWidth`, `FixedWidth`
- */
- case eightBytes = 1
-
- /**
- Decode a data type from an integer tag.
-
- The integer tag includes both the integer field key (or string key length) and the data type,
- where the data type is encoded in the three LSB.
- - Parameter value: The raw tag value.
- - Throws: `DecodingError.dataCorrupted()`, if the data type is unknown (3 or 4)
- */
- init(decodeFrom value: Int, path: [CodingKey]) throws {
- let rawDataType = value & 0x7
- guard let dataType = DataType(rawValue: rawDataType) else {
- let context = DecodingError.Context(codingPath: path, debugDescription: "Unknown data type \(rawDataType)")
- throw DecodingError.dataCorrupted(context)
- }
- self = dataType
- }
-
- /**
- Indicate that the datatype is also available in the protobuf specification.
-
- All data types except `.byte` and `.twoBytes` are compatible.
- */
- var isProtobufCompatible: Bool {
- switch self {
- case .variableLengthInteger, .variableLength, .fourBytes, .eightBytes:
- return true
- case .byte, .twoBytes:
- return false
- }
- }
-}
diff --git a/Sources/BinaryCodable/Common/SuperEncoderKey.swift b/Sources/BinaryCodable/Common/SuperCodingKey.swift
similarity index 95%
rename from Sources/BinaryCodable/Common/SuperEncoderKey.swift
rename to Sources/BinaryCodable/Common/SuperCodingKey.swift
index 1194533..d19dc77 100644
--- a/Sources/BinaryCodable/Common/SuperEncoderKey.swift
+++ b/Sources/BinaryCodable/Common/SuperCodingKey.swift
@@ -5,7 +5,7 @@ import Foundation
The key uses either the string key `super`, or the integer key `0`.
*/
-struct SuperEncoderKey: CodingKey {
+struct SuperCodingKey: CodingKey {
/**
Create a new super encoding key.
diff --git a/Sources/BinaryCodable/Decoding/AbstractDecodingNode.swift b/Sources/BinaryCodable/Decoding/AbstractDecodingNode.swift
index d80cb1b..1ce3a81 100644
--- a/Sources/BinaryCodable/Decoding/AbstractDecodingNode.swift
+++ b/Sources/BinaryCodable/Decoding/AbstractDecodingNode.swift
@@ -1,6 +1,25 @@
import Foundation
+/**
+ A class to provide decoding functions to all decoding containers.
+ */
class AbstractDecodingNode: AbstractNode {
-
+ let parentDecodedNil: Bool
+
+ init(parentDecodedNil: Bool, codingPath: [CodingKey], userInfo: UserInfo) {
+ self.parentDecodedNil = parentDecodedNil
+ super.init(codingPath: codingPath, userInfo: userInfo)
+ }
+
+ func decode(element: Data?, type: T.Type, codingPath: [CodingKey]) throws -> T where T: Decodable {
+ if let BaseType = T.self as? DecodablePrimitive.Type {
+ guard let element else {
+ throw DecodingError.valueNotFound(type, codingPath: codingPath, "Found nil instead of expected type \(type)")
+ }
+ return try BaseType.init(data: element, codingPath: codingPath) as! T
+ }
+ let node = try DecodingNode(data: element, parentDecodedNil: parentDecodedNil, codingPath: codingPath, userInfo: userInfo)
+ return try type.init(from: node)
+ }
}
diff --git a/Sources/BinaryCodable/Decoding/BinaryStreamBuffer.swift b/Sources/BinaryCodable/Decoding/BinaryStreamBuffer.swift
deleted file mode 100644
index 39531ba..0000000
--- a/Sources/BinaryCodable/Decoding/BinaryStreamBuffer.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-import Foundation
-
-final class BinaryStreamBuffer {
-
- private let data: BinaryStreamProvider
-
- private var buffer: Data
-
- private var index: Data.Index
-
- init(data: BinaryStreamProvider) {
- self.data = data
- self.buffer = Data()
- self.index = 0
- }
-
- private func getBufferedBytes(_ count: Int) -> Data {
- let newIndex = index + count
- guard newIndex <= buffer.endIndex else {
- return buffer[index.. Data {
- guard index <= buffer.endIndex else {
- return Data()
- }
- return buffer[index.. Data {
- let bufferedBytes = getBufferedBytes(count)
- let remaining = count - bufferedBytes.count
- guard remaining > 0 else {
- index += count
- return bufferedBytes
- }
- let remainingBytes = try data.getBytes(remaining, path: path)
- addToBuffer(remainingBytes)
- return bufferedBytes + remainingBytes
- }
-
- var hasMoreBytes: Bool {
- index < buffer.endIndex || data.hasMoreBytes
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/DataDecoder.swift b/Sources/BinaryCodable/Decoding/DataDecoder.swift
deleted file mode 100644
index adc540f..0000000
--- a/Sources/BinaryCodable/Decoding/DataDecoder.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-import Foundation
-
-final class DataDecoder: BinaryStreamProvider {
-
- let data: Data
-
- var index: Data.Index
-
- init(data: Data) {
- self.data = data
- self.index = data.startIndex
- }
-
- var hasMoreBytes: Bool {
- index < data.endIndex
- }
-
- func getAllData() -> Data {
- data
- }
-
- func getBytes(_ count: Int, path: [CodingKey]) throws -> Data {
- let newIndex = index + count
- guard newIndex <= data.endIndex else {
- throw DecodingError.prematureEndOfData(path)
- }
- defer { index = newIndex }
- return data[index.. Int {
- let bytesToRemove = min(buffer.count, bytes)
- buffer = buffer.dropFirst(bytesToRemove)
- index = buffer.startIndex
- return bytesToRemove
- }
-
- var totalBufferSize: Int {
- buffer.count
- }
-
- var unusedBytes: Int {
- buffer.endIndex - index
- }
-}
-
-// MARK: BinaryStreamProvider
-
-extension DataDecodingBuffer: BinaryStreamProvider {
-
- func getBytes(_ count: Int, path: [CodingKey]) throws -> Data {
- let newIndex = index + count
- guard newIndex <= buffer.endIndex else {
- didExceedBuffer = true
- throw DecodingError.prematureEndOfData(path)
- }
- defer { index = newIndex }
- return buffer[index.. UInt64
+
+ var numberOfRemainingBytes: Int { get }
+
+ func getBytes(_ count: Int) throws -> Data
+}
+
+extension DecodingDataProvider {
+
+ func remainingBytes() throws -> Data {
+ try getBytes(numberOfRemainingBytes)
+ }
+
+ /**
+ Decode an unsigned integer using variable-length encoding starting at a position.
+ */
+ func decodeUInt64() throws -> UInt64 {
+ let start = try nextByte()
+ return try decodeUInt64(startByte: start)
+ }
+
+ /**
+ Decode an unsigned integer using variable-length encoding starting at a position.
+ */
+ private func decodeUInt64(startByte: UInt64) throws -> UInt64 {
+ guard startByte & 0x80 > 0 else {
+ return startByte
+ }
+
+ var result = startByte & 0x7F
+ // There are always 7 usable bits per byte, for 8 bytes
+ for byteIndex in 1..<8 {
+ let nextByte = try nextByte()
+ // Insert the last 7 bit of the byte at the end
+ result += UInt64(nextByte & 0x7F) << (byteIndex*7)
+ // Check if an additional byte is coming
+ guard nextByte & 0x80 > 0 else {
+ return result
+ }
+ }
+
+ // The 9th byte has no next-byte bit, so all 8 bits are used
+ let nextByte = try nextByte()
+ result += UInt64(nextByte) << 56
+ return result
+ }
+
+ private func decodeNilIndicatorOrLength() throws -> Int? {
+ let first = try nextByte()
+
+ // Check the nil indicator bit
+ guard first & 0x01 == 0 else {
+ return nil
+ }
+ // The rest is the length, encoded as a varint
+ let rawLengthValue = try decodeUInt64(startByte: first)
+
+ // Remove the nil indicator bit
+ return Int(rawLengthValue >> 1)
+ }
+
+ func decodeNextDataOrNilElement() throws -> Data? {
+ guard let length = try decodeNilIndicatorOrLength() else {
+ return nil
+ }
+ return try getBytes(length)
+ }
+
+ func decodeUnkeyedElements() throws -> [Data?] {
+ var elements = [Data?]()
+ while !isAtEnd {
+ let element = try decodeNextDataOrNilElement()
+ elements.append(element)
+ }
+ return elements
+ }
+
+ func decodeNextKey() throws -> DecodingKey {
+ // First, decode the next key
+ let rawKeyOrLength = try decodeUInt64()
+ let lengthOrKey = Int(rawKeyOrLength >> 1)
+
+ guard rawKeyOrLength & 1 == 1 else {
+ // Int key
+ return .integer(lengthOrKey)
+ }
+
+ // String key, decode length bytes
+ let stringData = try getBytes(lengthOrKey)
+ let stringKey = try String(data: stringData, codingPath: codingPath)
+ return .string(stringKey)
+ }
+
+ func decodeKeyDataPairs() throws -> [DecodingKey : Data?] {
+ var elements = [DecodingKey : Data?]()
+ while !isAtEnd {
+ let key = try decodeNextKey()
+ guard !isAtEnd else {
+ throw corrupted("Unexpected end of data after decoding key")
+ }
+ let element = try decodeNextDataOrNilElement()
+ guard elements[key] == nil else {
+ throw corrupted("Found multiple values for key \(key)")
+ }
+ elements[key] = element
+ }
+ return elements
+ }
+
+
+ /**
+ Decode just the nil indicator byte, but don't extract a length. Uses all remaining bytes for the value.
+ - Note: This function is only used for the root node
+ */
+ func decodeSingleElementWithNilIndicator() throws -> Data? {
+ let first = try nextByte()
+ // Check the nil indicator bit
+ switch first {
+ case 0:
+ return try remainingBytes()
+ case 1:
+ guard isAtEnd else {
+ throw corrupted("\(numberOfRemainingBytes) additional bytes found after nil indicator")
+ }
+ return nil
+ default:
+ throw corrupted("Found unexpected nil indicator \(first)")
+ }
+ }
+
+ /**
+ Check if the value is `nil` or return the wrapped data.
+ - Note: This function is the equivalent to `encodedDataWithNilIndicatorAndLength()`.
+ */
+ func decodingSingleElementWithNilIndicatorAndLength() throws -> Data? {
+ let first = try nextByte()
+ // Check the nil indicator bit
+ guard first & 0x01 == 0 else {
+ return nil
+ }
+ // The rest is the length, encoded as a varint
+ let rawLengthValue = try decodeUInt64()
+ // Remove the nil indicator bit
+ let length = Int(rawLengthValue >> 1)
+ let wrappedElement = try getBytes(length)
+ guard isAtEnd else {
+ throw corrupted("Found \(numberOfRemainingBytes) unexpected bytes after encoded data")
+ }
+ return wrappedElement
+ }
+
+ func corrupted(_ message: String) -> DecodingError {
+ return .corrupted(message, codingPath: codingPath)
+ }
+}
diff --git a/Sources/BinaryCodable/Decoding/DecodingKey.swift b/Sources/BinaryCodable/Decoding/DecodingKey.swift
index 9889cb0..773039a 100644
--- a/Sources/BinaryCodable/Decoding/DecodingKey.swift
+++ b/Sources/BinaryCodable/Decoding/DecodingKey.swift
@@ -1,68 +1,50 @@
import Foundation
-enum DecodingKey {
- case intKey(Int)
- case stringKey(String)
+/**
+ A decoded key
+ */
+enum DecodingKey: Hashable {
- func isEqual(to key: CodingKey) -> Bool {
- switch self {
- case .intKey(let value):
- return value == key.intValue
- case .stringKey(let value):
- return value == key.stringValue
- }
- }
+ /// A decoded integer key
+ case integer(Int)
- private static func isStringKey(_ value: Int) -> Bool {
- value & 0x08 > 0
- }
+ /// A decoded string key
+ case string(String)
- private static func decodeKey(_ raw: Int, from decoder: BinaryStreamProvider, path: [CodingKey]) throws -> DecodingKey {
- let value = raw >> 4
- guard isStringKey(raw) else {
- return DecodingKey.intKey(value)
- }
- guard value >= 0 else {
- throw DecodingError.invalidDataSize(path)
+ func asKey(_ type: T.Type = T.self) -> T? where T: CodingKey {
+ switch self {
+ case .integer(let int):
+ return .init(intValue: int)
+ case .string(let string):
+ return .init(stringValue: string)
}
- let stringKeyData = try decoder.getBytes(value, path: path)
- let stringKey = try String(decodeFrom: stringKeyData, path: path)
- return DecodingKey.stringKey(stringKey)
}
+}
- static func decode(from decoder: BinaryStreamProvider, path: [CodingKey]) throws -> (key: DecodingKey, dataType: DataType) {
- let raw = try decoder.getVarint(path: path)
- let dataType = try DataType(decodeFrom: raw, path: path)
- let key = try decodeKey(raw, from: decoder, path: path)
- return (key, dataType)
- }
+extension DecodingKey {
- static func decodeProto(from decoder: BinaryStreamProvider, path: [CodingKey]) throws -> (key: DecodingKey, dataType: DataType) {
- let raw = try decoder.getVarint(path: path)
- let dataType = try DataType(decodeFrom: raw, path: path)
- let fieldNumber = raw >> 3
- try IntKeyWrapper.checkFieldBounds(fieldNumber)
- let key = DecodingKey.intKey(fieldNumber)
- return (key, dataType)
+ /**
+ Create a decoding key from an abstract coding key
+ */
+ init(key: CodingKey) {
+ if let intValue = key.intValue {
+ self = .integer(intValue)
+ } else {
+ self = .string(key.stringValue)
+ }
}
}
-extension DecodingKey: Equatable {
-
-}
-
-extension DecodingKey: Hashable {
+extension DecodingKey: ExpressibleByIntegerLiteral {
+ init(integerLiteral value: IntegerLiteralType) {
+ self = .integer(value)
+ }
}
-extension DecodingKey: CustomStringConvertible {
+extension DecodingKey: ExpressibleByStringLiteral {
- var description: String {
- switch self {
- case .intKey(let value):
- return "\(value)"
- case .stringKey(let value):
- return value
- }
+ init(stringLiteral value: StringLiteralType) {
+ self = .string(value)
}
}
diff --git a/Sources/BinaryCodable/Decoding/DecodingNode.swift b/Sources/BinaryCodable/Decoding/DecodingNode.swift
index 9b0d968..5b319b7 100644
--- a/Sources/BinaryCodable/Decoding/DecodingNode.swift
+++ b/Sources/BinaryCodable/Decoding/DecodingNode.swift
@@ -1,49 +1,59 @@
import Foundation
+/**
+ A class acting as a decoder, to provide different containers for decoding.
+ */
final class DecodingNode: AbstractDecodingNode, Decoder {
- private let storage: Storage
+ private let data: Data?
- private let isOptional: Bool
-
- private let isInUnkeyedContainer: Bool
+ private var didCallContainer = false
- init(storage: Storage, isOptional: Bool = false, path: [CodingKey], info: UserInfo, isInUnkeyedContainer: Bool = false) {
- self.storage = storage
- self.isOptional = isOptional
- self.isInUnkeyedContainer = isInUnkeyedContainer
- super.init(path: path, info: info)
+ init(data: Data?, parentDecodedNil: Bool, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) throws {
+ self.data = data
+ super.init(parentDecodedNil: parentDecodedNil, codingPath: codingPath, userInfo: userInfo)
}
- init(data: Data, isOptional: Bool = false, path: [CodingKey], info: UserInfo) {
- self.storage = .data(data)
- self.isOptional = isOptional
- self.isInUnkeyedContainer = false
- super.init(path: path, info: info)
+ private func registerContainer() throws {
+ guard !didCallContainer else {
+ throw DecodingError.corrupted("Multiple containers requested from decoder", codingPath: codingPath)
+ }
+ didCallContainer = true
}
- init(decoder: BinaryStreamProvider, isOptional: Bool = false, path: [CodingKey], info: UserInfo, isInUnkeyedContainer: Bool = false) {
- self.storage = .decoder(decoder)
- self.isOptional = isOptional
- self.isInUnkeyedContainer = isInUnkeyedContainer
- super.init(path: path, info: info)
+ private func getNonNilElement() throws -> Data {
+ try registerContainer()
+ // Non-root containers just use the data, which can't be nil
+ guard let data else {
+ throw DecodingError.corrupted("Container requested, but nil found", codingPath: codingPath)
+ }
+ return data
+ }
+
+ private func getPotentialNilElement() throws -> Data? {
+ try registerContainer()
+ guard !parentDecodedNil else {
+ return data
+ }
+ guard let data else {
+ return nil
+ }
+ return try DecodingStorage(data: data, codingPath: codingPath)
+ .decodeSingleElementWithNilIndicator()
}
func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey {
- let container = try KeyedDecoder(data: storage.useAsData(path: codingPath), path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
+ let data = try getNonNilElement()
+ return KeyedDecodingContainer(try KeyedDecoder(data: data, codingPath: codingPath, userInfo: userInfo))
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
- return try UnkeyedDecoder(data: storage.useAsData(path: codingPath), path: codingPath, info: userInfo)
+ let data = try getNonNilElement()
+ return try UnkeyedDecoder(data: data, codingPath: codingPath, userInfo: userInfo)
}
func singleValueContainer() throws -> SingleValueDecodingContainer {
- return ValueDecoder(
- storage: storage,
- isOptional: isOptional,
- isInUnkeyedContainer: isInUnkeyedContainer,
- path: codingPath,
- info: userInfo)
+ let data = try getPotentialNilElement()
+ return ValueDecoder(data: data, codingPath: codingPath, userInfo: userInfo)
}
}
diff --git a/Sources/BinaryCodable/Decoding/DecodingStorage.swift b/Sources/BinaryCodable/Decoding/DecodingStorage.swift
index c33473b..3609358 100644
--- a/Sources/BinaryCodable/Decoding/DecodingStorage.swift
+++ b/Sources/BinaryCodable/Decoding/DecodingStorage.swift
@@ -1,24 +1,44 @@
import Foundation
-enum Storage {
- case data(Data)
- case decoder(BinaryStreamProvider)
-
- func useAsData(path: [CodingKey]) throws -> Data {
- switch self {
- case .data(let data):
- return data
- case .decoder(let decoder):
- return try decoder.getData(for: .variableLength, path: path)
+final class DecodingStorage {
+
+ let codingPath: [CodingKey]
+
+ private let data: Data
+
+ private var index: Data.Index
+
+ init(data: Data, codingPath: [CodingKey]) {
+ self.codingPath = codingPath
+ self.data = data
+ self.index = data.startIndex
+ }
+}
+
+extension DecodingStorage: DecodingDataProvider {
+
+ var isAtEnd: Bool {
+ index >= data.endIndex
+ }
+
+ func nextByte() throws -> UInt64 {
+ guard !isAtEnd else {
+ throw corrupted("Missing byte(s) decoding variable length integer")
}
+ defer { index += 1 }
+ return UInt64(data[index])
+ }
+
+ var numberOfRemainingBytes: Int {
+ data.endIndex - index
}
- func useAsDecoder() -> BinaryStreamProvider {
- switch self {
- case .data(let data):
- return DataDecoder(data: data)
- case .decoder(let decoder):
- return decoder
+ func getBytes(_ count: Int) throws -> Data {
+ let newIndex = index + count
+ guard newIndex <= data.endIndex else {
+ throw corrupted("Unexpected end of data")
}
+ defer { index = newIndex }
+ return data[index..: AbstractDecodingNode, KeyedDecodingContainerProtocol where Key: CodingKey {
- let content: [DecodingKey: Data]
+ let allKeys: [Key]
- init(data: Data, path: [CodingKey], info: UserInfo) throws {
- let decoder = DataDecoder(data: data)
- var content = [DecodingKey: [Data]]()
- while decoder.hasMoreBytes {
- let (key, dataType) = try DecodingKey.decode(from: decoder, path: path)
+ /// - Note: The keys are not of type `Key`, since `CodingKey`s are not `Hashable`.
+ /// Also, some keys found in the data may not be convertable to `Key`, e.g. the `super` key, or obsoleted keys from older implementations.
+ private let elements: [DecodingKey: Data?]
- do {
- let data = try decoder.getData(for: dataType, path: path)
- guard content[key] != nil else {
- content[key] = [data]
- continue
- }
- } catch DecodingError.dataCorrupted(let context) {
- let codingKey = {
- switch key {
- case .stringKey(let stringValue):
- return Key(stringValue: stringValue)
- case .intKey(let intValue):
- return Key(intValue: intValue)
- }
- }()
- var newCodingPath = path
- if let codingKey {
- newCodingPath += [codingKey]
- }
- let newContext = DecodingError.Context(
- codingPath: newCodingPath,
- debugDescription: context.debugDescription,
- underlyingError: context.underlyingError
- )
- throw DecodingError.dataCorrupted(newContext)
- }
-
- throw DecodingError.multipleValuesForKey(path, key)
- }
- self.content = content.mapValues { parts in
- guard parts.count > 1 else {
- return parts[0]
- }
- /// We only get here when `forceProtobufCompatibility = true`
- /// So we need to prepend the length of each element
- /// so that `KeyedEncoder` can decode it correctly
- return parts.map {
- $0.count.variableLengthEncoding + $0
- }.joinedData
- }
- super.init(path: path, info: info)
+ init(data: Data, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) throws {
+ self.elements = try DecodingStorage(data: data, codingPath: codingPath).decodeKeyDataPairs()
+ self.allKeys = elements.keys.compactMap { $0.asKey() }
+ super.init(parentDecodedNil: true, codingPath: codingPath, userInfo: userInfo)
}
- var allKeys: [Key] {
- content.keys.compactMap { key in
- switch key {
- case .intKey(let value):
- return Key(intValue: value)
- case .stringKey(let value):
- return Key(stringValue: value)
- }
+ private func value(for intKey: Int?) -> Data?? {
+ guard let intKey else {
+ return nil
}
+ return elements[.integer(intKey)]
}
- func contains(_ key: Key) -> Bool {
- content.keys.contains { $0.isEqual(to: key) }
+ private func value(for stringKey: String) -> Data?? {
+ elements[.string(stringKey)]
}
- private func getData(forKey key: CodingKey) throws -> Data {
- guard let data = content.first(where: { $0.key.isEqual(to: key) })?.value else {
- let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Key not found")
- throw DecodingError.keyNotFound(key, context)
+ private func value(for key: CodingKey) throws -> Data? {
+ let int = value(for: key.intValue)
+ let string = value(for: key.stringValue)
+ if int != nil && string != nil {
+ throw DecodingError.corrupted("Found value for int and string key", codingPath: codingPath + [key])
}
- return data
- }
-
- func decodeNil(forKey key: Key) throws -> Bool {
- !contains(key)
+ guard let value = int ?? string else {
+ throw DecodingError.notFound(key, codingPath: codingPath + [key], "Missing value for key")
+ }
+ return value
}
- func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
- try wrapError(forKey: key) {
- if let optionalType = type as? AnyOptional.Type {
- if let data = try? getData(forKey: key) {
- let node = DecodingNode(data: data, isOptional: true, path: codingPath, info: userInfo)
- return try T.init(from: node)
- } else {
- return optionalType.nilValue as! T
- }
- } else if let Primitive = type as? DecodablePrimitive.Type {
- let data = try getData(forKey: key)
- return try Primitive.init(decodeFrom: data, path: codingPath + [key]) as! T
- } else {
- let data = try getData(forKey: key)
- let node = DecodingNode(data: data, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
- }
+ private func node(for key: CodingKey) throws -> DecodingNode {
+ let element = try value(for: key)
+ return try DecodingNode(data: element, parentDecodedNil: true, codingPath: codingPath + [key], userInfo: userInfo)
}
func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
- try wrapError(forKey: key) {
- let data = try getData(forKey: key)
- let container = try KeyedDecoder(data: data, path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
+ KeyedDecodingContainer(try node(for: key).container(keyedBy: type))
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
- try wrapError(forKey: key) {
- let data = try getData(forKey: key)
- return try UnkeyedDecoder(data: data, path: codingPath, info: userInfo)
- }
+ try node(for: key).unkeyedContainer()
}
func superDecoder() throws -> Decoder {
- let data = try getData(forKey: SuperEncoderKey())
- return DecodingNode(data: data, path: codingPath, info: userInfo)
+ try node(for: SuperCodingKey())
}
func superDecoder(forKey key: Key) throws -> Decoder {
- let data = try getData(forKey: key)
- return DecodingNode(data: data, path: codingPath, info: userInfo)
+ try node(for: key)
}
- private func wrapError(forKey key: Key, _ block: () throws -> T) throws -> T {
- do {
- return try block()
- } catch DecodingError.dataCorrupted(let context) {
- let newContext = DecodingError.Context(
- codingPath: codingPath + [key] + context.codingPath,
- debugDescription: context.debugDescription,
- underlyingError: context.underlyingError
- )
- throw DecodingError.dataCorrupted(newContext)
+ func contains(_ key: Key) -> Bool {
+ if let intValue = key.intValue, elements[.integer(intValue)] != .none {
+ return true
}
+ return elements[.string(key.stringValue)] != .none
+ }
+
+ /**
+ Decodes a null value for the given key.
+ - Parameter key: The key that the decoded value is associated with.
+ - Returns: Whether the encountered key is contained
+ */
+ func decodeNil(forKey key: Key) -> Bool {
+ /**
+ **Important note**: The implementation of `encodeNil(forKey:)` and `decodeNil(forKey:)` are implemented differently than the `Codable` documentation specifies:
+ - Throws: `DecodingError.keyNotFound` if `self` does not have an entry for the given key.
+
+ If a value is `nil`, then it is not encoded.
+ We could change this by explicitly assigning a `nil` value during encoding.
+ But it would cause other problems, either breaking the decoding of double optionals (e.g. Int??),
+ or requiring an additional `nil` indicator for **all** values in keyed containers,
+ which would make the format less efficient.
+
+ The alternative would be:
+ ```
+ try value(for: key) == nil
+ ```
+ */
+ !contains(key)
+ }
+
+ func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
+ let element = try value(for: key)
+ return try decode(element: element, type: type, codingPath: codingPath + [key])
}
}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictDecodingNode.swift b/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictDecodingNode.swift
deleted file mode 100644
index 74dbf40..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictDecodingNode.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import Foundation
-
-final class ProtoDictDecodingNode: ProtoDecodingNode {
-
- override func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey {
- let container = try ProtoDictKeyedDecoder(data: storage.useAsData(path: codingPath), path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
-
- override func unkeyedContainer() throws -> UnkeyedDecodingContainer {
- try ProtoDictUnkeyedDecoder(data: storage.useAsData(path: codingPath), path: codingPath, info: userInfo)
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictKeyedDecoder.swift b/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictKeyedDecoder.swift
deleted file mode 100644
index 20da9a1..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictKeyedDecoder.swift
+++ /dev/null
@@ -1,51 +0,0 @@
-import Foundation
-
-final class ProtoDictKeyedDecoder: ProtoKeyedDecoder where Key: CodingKey {
-
- override init(data: Data, path: [CodingKey], info: UserInfo) throws {
- let decoder = DataDecoder(data: data)
- var content = [DecodingKey: [Data]]()
- while decoder.hasMoreBytes {
- let pairData = try decoder.getData(for: .variableLength, path: path)
- let pairDecoder = DataDecoder(data: pairData)
- let (keyField, keyDataType) = try DecodingKey.decodeProto(from: pairDecoder, path: path)
- guard case .intKey(1) = keyField else {
- throw ProtobufDecodingError.unexpectedDictionaryKey
- }
- let keyData = try pairDecoder.getData(for: keyDataType, path: path)
- let key: DecodingKey
- switch keyDataType {
- case .variableLengthInteger:
- let value = try Int(decodeFrom: keyData, path: path)
- key = .intKey(value)
- case .variableLength:
- let value = try String(decodeFrom: keyData, path: path)
- key = .stringKey(value)
- default:
- throw ProtobufDecodingError.unexpectedDictionaryKey
- }
-
- let (valueField, valueDataType) = try DecodingKey.decodeProto(from: pairDecoder, path: path)
- guard case .intKey(2) = valueField else {
- throw ProtobufDecodingError.unexpectedDictionaryKey
- }
- let valueData = try pairDecoder.getData(for: valueDataType, path: path)
- guard content[key] != nil else {
- content[key] = [valueData]
- continue
- }
- content[key]!.append(valueData)
- }
- let mapped = content.mapValues { parts -> Data in
- guard parts.count > 1 else {
- return parts[0]
- }
- /// We need to prepend the length of each element
- /// so that `KeyedEncoder` can decode it correctly
- return parts.map {
- $0.count.variableLengthEncoding + $0
- }.joinedData
- }
- super.init(content: mapped, path: path, info: info)
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictUnkeyedDecoder.swift b/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictUnkeyedDecoder.swift
deleted file mode 100644
index 3e8c5b5..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/Dictionary/ProtoDictUnkeyedDecoder.swift
+++ /dev/null
@@ -1,91 +0,0 @@
-import Foundation
-
-final class ProtoDictUnkeyedDecoder: AbstractDecodingNode, UnkeyedDecodingContainer {
-
- let elements: [(dataType: DataType, data: Data)]
-
- init(data: Data, path: [CodingKey], info: UserInfo) throws {
- let decoder = DataDecoder(data: data)
- var elements = [(dataType: DataType, data: Data)]()
- while decoder.hasMoreBytes {
- let pairData = try decoder.getData(for: .variableLength, path: path)
- let pairDecoder = DataDecoder(data: pairData)
-
- let (keyField, keyDataType) = try DecodingKey.decodeProto(from: pairDecoder, path: path)
- guard case .intKey(1) = keyField else {
- throw ProtobufDecodingError.unexpectedDictionaryKey
- }
- let keyData = try pairDecoder.getData(for: keyDataType, path: path)
- elements.append((dataType: keyDataType, data: keyData))
-
- let (valueField, valueDataType) = try DecodingKey.decodeProto(from: pairDecoder, path: path)
- guard case .intKey(2) = valueField else {
- throw ProtobufDecodingError.unexpectedDictionaryKey
- }
- let valueData = try pairDecoder.getData(for: valueDataType, path: path)
- elements.append((dataType: valueDataType, data: valueData))
- }
- self.elements = elements
- super.init(path: path, info: info)
- }
-
- var count: Int? {
- elements.count
- }
-
- var isAtEnd: Bool {
- currentIndex == elements.count
- }
-
- var currentIndex: Int = 0
-
- func decodeNil() -> Bool {
- return false
- }
-
- private var currentElement: (dataType: DataType, data: Data) {
- elements[currentIndex]
- }
-
- private func getCurrentElementVariableLengthData() throws -> Data {
- let element = currentElement
- guard element.dataType == .variableLength else {
- throw ProtobufDecodingError.unexpectedDictionaryKey
- }
- return element.data
- }
-
- func decode(_ type: T.Type) throws -> T where T : Decodable {
- defer { currentIndex += 1 }
- if let Primitive = type as? DecodablePrimitive.Type {
- let element = currentElement
- guard element.dataType == Primitive.dataType else {
- throw ProtobufDecodingError.unexpectedDictionaryKey
- }
- if let ProtoType = type as? ProtobufDecodable.Type {
- return try ProtoType.init(fromProtobuf: element.data, path: codingPath) as! T
- }
- throw ProtobufDecodingError.unsupported(type: type)
- }
- let decoder = DataDecoder(data: try getCurrentElementVariableLengthData())
- let node = ProtoDecodingNode(decoder: decoder, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
-
- func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
- defer { currentIndex += 1 }
- let data = try getCurrentElementVariableLengthData()
- let container = try ProtoKeyedDecoder(data: data, path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
-
- func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
- defer { currentIndex += 1 }
- let data = try getCurrentElementVariableLengthData()
- return try ProtoUnkeyedDecoder(data: data, path: codingPath, info: userInfo)
- }
-
- func superDecoder() throws -> Decoder {
- throw ProtobufDecodingError.superNotSupported
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfAssociatedValuesDecoder.swift b/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfAssociatedValuesDecoder.swift
deleted file mode 100644
index 613d451..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfAssociatedValuesDecoder.swift
+++ /dev/null
@@ -1,70 +0,0 @@
-import Foundation
-
-/**
- A keyed decoder specifically to decode the associated value within a `ProtobufOneOf` type.
-
- The decoder receives the data associated with the enum case from `OneOfKeyedDecoder`,
- and allows exactly one associated value key (`_0`) to be decoded from the data.
-
- The decoder allows only one call to `decode(_:forKey:)`, all other access will fail.
- */
-final class OneOfAssociatedValuesDecoder: AbstractDecodingNode, KeyedDecodingContainerProtocol where Key: CodingKey {
-
- private let data: Data
-
- init(data: Data, path: [CodingKey], info: UserInfo) {
- self.data = data
- super.init(path: path, info: info)
- }
-
- var allKeys: [Key] {
- [Key(stringValue: "_0")!]
- }
-
- func contains(_ key: Key) -> Bool {
- return true
- }
-
- func decodeNil(forKey key: Key) throws -> Bool {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `decodeNil(forKey:)` while decoding associated value for a `ProtobufOneOf` type")
- }
-
- func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
- if let _ = type as? DecodablePrimitive.Type {
- if let ProtoType = type as? ProtobufDecodable.Type {
- if !data.isEmpty {
- return try ProtoType.init(fromProtobuf: data, path: codingPath + [key]) as! T
- } else {
- return ProtoType.zero as! T
- }
- }
- throw ProtobufDecodingError.unsupported(type: type)
- } else if type is AnyDictionary.Type {
- let node = ProtoDictDecodingNode(data: data, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
- let node = ProtoDecodingNode(data: data, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
-
- func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `nestedContainer(keyedBy:forKey:)` while decoding associated value for a `ProtobufOneOf` type")
- }
-
- func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `nestedUnkeyedContainer(forKey:)` while decoding associated value for a `ProtobufOneOf` type")
- }
-
- func superDecoder() throws -> Decoder {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `superDecoder()` while decoding associated value for a `ProtobufOneOf` type")
- }
-
- func superDecoder(forKey key: Key) throws -> Decoder {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `superDecoder(forKey:)` while decoding associated value for a `ProtobufOneOf` type")
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfDecodingNode.swift b/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfDecodingNode.swift
deleted file mode 100644
index fa1120f..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfDecodingNode.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-import Foundation
-
-typealias KeyedDataCallback = ((CodingKey) -> Data?)
-
-/**
- A decoding node specifically to decode protobuf `OneOf` structures.
-
- The node receives all data from the parent container, and hands it to a `OneOfKeyedDecoder`,
- which then decodes the keyed values within the enum.
-
- Attempts to access other containers (`keyed` or `single`) will throw an error.
- */
-final class OneOfDecodingNode: AbstractDecodingNode, Decoder {
-
- private let content: [DecodingKey: Data]
-
- init(content: [DecodingKey: Data], path: [CodingKey], info: UserInfo) {
- self.content = content
- super.init(path: path, info: info)
- }
-
- func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey {
- let container = OneOfKeyedDecoder(content: content, path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
-
- func unkeyedContainer() throws -> UnkeyedDecodingContainer {
- throw ProtobufDecodingError.invalidAccess(
- "Attempt to access unkeyedContainer() for a type conforming to ProtobufOneOf")
- }
-
- func singleValueContainer() throws -> SingleValueDecodingContainer {
- throw ProtobufDecodingError.invalidAccess(
- "Attempt to access singleValueContainer() for a type conforming to ProtobufOneOf")
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfKeyedDecoder.swift b/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfKeyedDecoder.swift
deleted file mode 100644
index 3ee2175..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/OneOf/OneOfKeyedDecoder.swift
+++ /dev/null
@@ -1,66 +0,0 @@
-import Foundation
-
-/**
- A keyed decoder specifically to decode an enum of type `ProtobufOneOf`.
-
- The decoder only allows a call to `nestedContainer(keyedBy:forKey:)`
- to decode the associated value of the `OneOf`, all other access will fail with an error.
-
- The decoder receives all key-value pairs from the parent node (passed through `OneOfDecodingNode`),
- in order to select the appropriate data required to decode the enum (`OneOf` types share the field values with the enclosing message).
- */
-final class OneOfKeyedDecoder: AbstractDecodingNode, KeyedDecodingContainerProtocol where Key: CodingKey {
-
- private let content: [DecodingKey: Data]
-
- init(content: [DecodingKey: Data], path: [CodingKey], info: UserInfo) {
- self.content = content
- super.init(path: path, info: info)
- }
-
- var allKeys: [Key] {
- content.keys.compactMap { key in
- switch key {
- case .intKey(let value):
- return Key(intValue: value)
- case .stringKey(let value):
- return Key(stringValue: value)
- }
- }
- }
-
- func contains(_ key: Key) -> Bool {
- content.keys.contains { $0.isEqual(to: key) }
- }
-
- func decodeNil(forKey key: Key) throws -> Bool {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `decodeNil(forKey:)` while decoding `ProtobufOneOf` enum")
- }
-
- func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `decode(_,forKey)` while decoding `ProtobufOneOf` enum")
- }
-
- func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
- let data = content.first(where: { $0.key.isEqual(to: key) })?.value ?? Data()
- let container = OneOfAssociatedValuesDecoder(data: data, path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
-
- func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `nestedUnkeyedContainer(forKey:)` while decoding `ProtobufOneOf` enum")
- }
-
- func superDecoder() throws -> Decoder {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `superDecoder()` while decoding `ProtobufOneOf` enum")
- }
-
- func superDecoder(forKey key: Key) throws -> Decoder {
- throw ProtobufDecodingError.invalidAccess(
- "Unexpected call to `superDecoder(forKey:)` while decoding `ProtobufOneOf` enum")
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/ProtoDecodingNode.swift b/Sources/BinaryCodable/Decoding/Protobuf/ProtoDecodingNode.swift
deleted file mode 100644
index a242023..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/ProtoDecodingNode.swift
+++ /dev/null
@@ -1,32 +0,0 @@
-import Foundation
-
-class ProtoDecodingNode: AbstractDecodingNode, Decoder {
-
- let storage: Storage
-
- init(data: Data, path: [CodingKey], info: UserInfo) {
- self.storage = .data(data)
- super.init(path: path, info: info)
- }
-
- init(decoder: BinaryStreamProvider, path: [CodingKey], info: UserInfo) {
- self.storage = .decoder(decoder)
- super.init(path: path, info: info)
- }
-
- func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey {
- let container = try ProtoKeyedDecoder(data: storage.useAsData(path: codingPath), path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
-
- func unkeyedContainer() throws -> UnkeyedDecodingContainer {
- return try ProtoUnkeyedDecoder(data: storage.useAsData(path: codingPath), path: codingPath, info: userInfo)
- }
-
- func singleValueContainer() throws -> SingleValueDecodingContainer {
- return ProtoValueDecoder(
- data: storage.useAsDecoder(),
- path: codingPath,
- info: userInfo)
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/ProtoKeyedDecoder.swift b/Sources/BinaryCodable/Decoding/Protobuf/ProtoKeyedDecoder.swift
deleted file mode 100644
index 48249da..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/ProtoKeyedDecoder.swift
+++ /dev/null
@@ -1,107 +0,0 @@
-import Foundation
-
-class ProtoKeyedDecoder: AbstractDecodingNode, KeyedDecodingContainerProtocol where Key: CodingKey {
-
- let content: [DecodingKey: Data]
-
- init(data: Data, path: [CodingKey], info: UserInfo) throws {
- let decoder = DataDecoder(data: data)
- var content = [DecodingKey: [Data]]()
- while decoder.hasMoreBytes {
- let (key, dataType) = try DecodingKey.decodeProto(from: decoder, path: path)
-
- let data = try decoder.getData(for: dataType, path: path)
-
- guard content[key] != nil else {
- content[key] = [data]
- continue
- }
- content[key]!.append(data)
- }
- self.content = content.mapValues { parts in
- guard parts.count > 1 else {
- return parts[0]
- }
- /// We need to prepend the length of each element
- /// so that `KeyedEncoder` can decode it correctly
- return parts.map {
- $0.count.variableLengthEncoding + $0
- }.joinedData
- }
- super.init(path: path, info: info)
- }
-
- init(content: [DecodingKey: Data], path: [CodingKey], info: UserInfo) {
- self.content = content
- super.init(path: path, info: info)
- }
-
- var allKeys: [Key] {
- content.keys.compactMap { key in
- switch key {
- case .intKey(let value):
- return Key(intValue: value)
- case .stringKey(let value):
- return Key(stringValue: value)
- }
- }
- }
-
- func contains(_ key: Key) -> Bool {
- content.keys.contains { $0.isEqual(to: key) }
- }
-
- private func getDataIfAvailable(forKey key: CodingKey) -> Data? {
- content.first(where: { $0.key.isEqual(to: key) })?.value
- }
-
- func getData(forKey key: CodingKey) -> Data {
- getDataIfAvailable(forKey: key) ?? Data()
- }
-
- func decodeNil(forKey key: Key) throws -> Bool {
- !contains(key)
- }
-
- func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
- if type is ProtobufOneOf.Type {
- let node = OneOfDecodingNode(content: content, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
- let data = getDataIfAvailable(forKey: key)
- if let _ = type as? DecodablePrimitive.Type {
- if let ProtoType = type as? ProtobufDecodable.Type {
- if let data = data {
- return try ProtoType.init(fromProtobuf: data, path: codingPath + [key]) as! T
- } else {
- return ProtoType.zero as! T
- }
- }
- throw ProtobufDecodingError.unsupported(type: type)
- } else if type is AnyDictionary.Type {
- let node = ProtoDictDecodingNode(data: data ?? Data(), path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
- let node = ProtoDecodingNode(data: data ?? Data(), path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
-
- func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
- let data = getData(forKey: key)
- let container = try ProtoKeyedDecoder(data: data, path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
-
- func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
- let data = getData(forKey: key)
- return try ProtoUnkeyedDecoder(data: data, path: codingPath, info: userInfo)
- }
-
- func superDecoder() throws -> Decoder {
- throw ProtobufDecodingError.superNotSupported
- }
-
- func superDecoder(forKey key: Key) throws -> Decoder {
- throw ProtobufDecodingError.superNotSupported
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/ProtoUnkeyedDecoder.swift b/Sources/BinaryCodable/Decoding/Protobuf/ProtoUnkeyedDecoder.swift
deleted file mode 100644
index e3944a5..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/ProtoUnkeyedDecoder.swift
+++ /dev/null
@@ -1,57 +0,0 @@
-import Foundation
-
-final class ProtoUnkeyedDecoder: AbstractDecodingNode, UnkeyedDecodingContainer {
-
- private let decoder: BinaryStreamProvider
-
- init(data: Data, path: [CodingKey], info: UserInfo) throws {
- self.decoder = DataDecoder(data: data)
- super.init(path: path, info: info)
- }
-
- var count: Int? {
- nil
- }
-
- var isAtEnd: Bool {
- !decoder.hasMoreBytes
- }
-
- var currentIndex: Int = 0
-
- func decodeNil() -> Bool {
- return false
- }
-
- func decode(_ type: T.Type) throws -> T where T : Decodable {
- defer { currentIndex += 1 }
- if let Primitive = type as? DecodablePrimitive.Type {
- let dataType = Primitive.dataType
- let data = try decoder.getData(for: dataType, path: codingPath)
- if let ProtoType = type as? ProtobufDecodable.Type {
- return try ProtoType.init(fromProtobuf: data, path: codingPath) as! T
- }
- throw ProtobufDecodingError.unsupported(type: type)
- }
- let node = ProtoDecodingNode(decoder: decoder, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
-
- func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
- currentIndex += 1
- let data = try decoder.getData(for: .variableLength, path: codingPath)
- let container = try ProtoKeyedDecoder(data: data, path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
-
- func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
- currentIndex += 1
- let data = try decoder.getData(for: .variableLength, path: codingPath)
- return try ProtoUnkeyedDecoder(data: data, path: codingPath, info: userInfo)
- }
-
- func superDecoder() throws -> Decoder {
- currentIndex += 1
- return ProtoDecodingNode(decoder: decoder, path: codingPath, info: userInfo)
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/Protobuf/ProtoValueDecoder.swift b/Sources/BinaryCodable/Decoding/Protobuf/ProtoValueDecoder.swift
deleted file mode 100644
index 2a30197..0000000
--- a/Sources/BinaryCodable/Decoding/Protobuf/ProtoValueDecoder.swift
+++ /dev/null
@@ -1,31 +0,0 @@
-import Foundation
-
-final class ProtoValueDecoder: AbstractDecodingNode, SingleValueDecodingContainer {
-
- let data: BinaryStreamProvider
-
- init(data: BinaryStreamProvider, path: [CodingKey], info: UserInfo) {
- self.data = data
- super.init(path: path, info: info)
- }
-
- func decodeNil() -> Bool {
- false
- }
-
- func decode(_ type: T.Type) throws -> T where T : Decodable {
- guard let Primitive = type as? DecodablePrimitive.Type else {
- let node = ProtoDecodingNode(decoder: data, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
-
- guard let ProtoType = Primitive as? ProtobufDecodable.Type else {
- throw ProtobufDecodingError.unsupported(type: type)
- }
- guard self.data.hasMoreBytes else {
- return ProtoType.zero as! T
- }
- let data = try self.data.getData(for: Primitive.dataType, path: codingPath)
- return try ProtoType.init(fromProtobuf: data, path: codingPath) as! T
- }
-}
diff --git a/Sources/BinaryCodable/Decoding/UnkeyedDecoder.swift b/Sources/BinaryCodable/Decoding/UnkeyedDecoder.swift
index d324434..4593498 100644
--- a/Sources/BinaryCodable/Decoding/UnkeyedDecoder.swift
+++ b/Sources/BinaryCodable/Decoding/UnkeyedDecoder.swift
@@ -2,104 +2,61 @@ import Foundation
final class UnkeyedDecoder: AbstractDecodingNode, UnkeyedDecodingContainer {
- private let decoder: BinaryStreamProvider
+ var count: Int? { data.count }
- private let nilIndices: Set
+ var isAtEnd: Bool { currentIndex >= data.count }
- init(data: Data, path: [CodingKey], info: UserInfo) throws {
- let decoder = DataDecoder(data: data)
- self.decoder = decoder
- if info.has(.prependNilIndicesForUnkeyedContainers) {
- let nilIndicesCount = try decoder.getVarint(path: path)
- guard nilIndicesCount >= 0 else {
- throw DecodingError.invalidDataSize(path)
- }
- self.nilIndices = try (0.. Data? {
+ guard currentIndex < data.count else {
+ throw DecodingError.corrupted("No more elements to decode", codingPath: codingPath)
+ }
+ return data[currentIndex]
}
- func decodeNil() throws -> Bool {
- guard prependNilIndexSetForUnkeyedContainers else {
- fatalError("Use option `containsNilIndexSetForUnkeyedContainers` to use `decodeNil()`")
- }
- guard nilIndices.contains(currentIndex) else {
- return false
- }
+ /// Get the next element and advance the index.
+ private func nextElement() throws -> Data? {
+ let element = try ensureNextElement()
currentIndex += 1
- return true
+ return element
}
- func decode(_ type: T.Type) throws -> T where T : Decodable {
- defer { currentIndex += 1 }
- return try wrapError {
- if type is AnyOptional.Type {
- let node = DecodingNode(decoder: decoder, isOptional: true, path: codingPath, info: userInfo, isInUnkeyedContainer: true)
- return try T.init(from: node)
- } else if let Primitive = type as? DecodablePrimitive.Type {
- let dataType = Primitive.dataType
- let data = try decoder.getData(for: dataType, path: codingPath)
- return try Primitive.init(decodeFrom: data, path: codingPath) as! T
- } else {
- let node = DecodingNode(decoder: decoder, path: codingPath, info: userInfo, isInUnkeyedContainer: true)
- return try T.init(from: node)
- }
- }
+ private func nextNode() throws -> DecodingNode {
+ let element = try nextElement()
+ return try DecodingNode(data: element, parentDecodedNil: true, codingPath: codingPath, userInfo: userInfo)
}
func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer where NestedKey : CodingKey {
- currentIndex += 1
- return try wrapError {
- let data = try decoder.getData(for: .variableLength, path: codingPath)
- let container = try KeyedDecoder(data: data, path: codingPath, info: userInfo)
- return KeyedDecodingContainer(container)
- }
+ KeyedDecodingContainer(try nextNode().container(keyedBy: type))
}
func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
- currentIndex += 1
- return try wrapError {
- let data = try decoder.getData(for: .variableLength, path: codingPath)
- return try UnkeyedDecoder(data: data, path: codingPath, info: userInfo)
- }
+ try nextNode().unkeyedContainer()
}
func superDecoder() throws -> Decoder {
- currentIndex += 1
- return DecodingNode(decoder: decoder, path: codingPath, info: userInfo, isInUnkeyedContainer: true)
+ try nextNode()
}
- private func wrapError(_ block: () throws -> T) throws -> T {
- do {
- return try block()
- } catch DecodingError.dataCorrupted(let context) {
- var codingPath = codingPath
- codingPath.append(AnyCodingKey(intValue: currentIndex))
- codingPath.append(contentsOf: context.codingPath)
- let newContext = DecodingError.Context(
- codingPath: codingPath,
- debugDescription: context.debugDescription,
- underlyingError: context.underlyingError
- )
- throw DecodingError.dataCorrupted(newContext)
+ func decodeNil() throws -> Bool {
+ if try ensureNextElement() == nil {
+ currentIndex += 1
+ return true
}
+ return false
+ }
+
+ func decode(_ type: T.Type) throws -> T where T : Decodable {
+ let element = try nextElement()
+ return try decode(element: element, type: type, codingPath: codingPath)
}
}
diff --git a/Sources/BinaryCodable/Decoding/ValueDecoder.swift b/Sources/BinaryCodable/Decoding/ValueDecoder.swift
index 41b242e..fd3961a 100644
--- a/Sources/BinaryCodable/Decoding/ValueDecoder.swift
+++ b/Sources/BinaryCodable/Decoding/ValueDecoder.swift
@@ -2,59 +2,18 @@ import Foundation
final class ValueDecoder: AbstractDecodingNode, SingleValueDecodingContainer {
- private var storage: Storage
+ private let data: Data?
- private let isOptional: Bool
-
- private let isInUnkeyedContainer: Bool
-
- init(storage: Storage, isOptional: Bool, isInUnkeyedContainer: Bool, path: [CodingKey], info: UserInfo) {
- self.storage = storage
- self.isOptional = isOptional
- self.isInUnkeyedContainer = isInUnkeyedContainer
- super.init(path: path, info: info)
- }
-
- private func asDecoder() -> BinaryStreamProvider {
- let decoder = self.storage.useAsDecoder()
- self.storage = .decoder(decoder)
- return decoder
+ init(data: Data?, codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
+ self.data = data
+ super.init(parentDecodedNil: false, codingPath: codingPath, userInfo: userInfo)
}
func decodeNil() -> Bool {
- do {
- let byte = try asDecoder().getByte(path: codingPath)
- return byte == 0
- } catch {
- return false
- }
+ data == nil
}
func decode(_ type: T.Type) throws -> T where T : Decodable {
- if type is AnyOptional.Type {
- let node = DecodingNode(storage: storage, isOptional: true, path: codingPath, info: userInfo)
- return try T.init(from: node)
- } else if let Primitive = type as? DecodablePrimitive.Type {
- let data: Data
- if !isInUnkeyedContainer, Primitive.dataType == .variableLength, !isOptional {
- switch storage {
- case .data(let d):
- data = d
- case .decoder(let decoder):
- if let d = decoder as? DataDecoder {
- data = d.getAllData()
- } else {
- data = try decoder.getData(for: Primitive.dataType, path: codingPath)
- }
- }
- } else {
- let decoder = asDecoder()
- data = try decoder.getData(for: Primitive.dataType, path: codingPath)
- }
- return try Primitive.init(decodeFrom: data, path: codingPath) as! T
- } else {
- let node = DecodingNode(storage: storage, path: codingPath, info: userInfo)
- return try T.init(from: node)
- }
+ try decode(element: data, type: type, codingPath: codingPath)
}
}
diff --git a/Sources/BinaryCodable/Encoding/AbstractEncodingNode.swift b/Sources/BinaryCodable/Encoding/AbstractEncodingNode.swift
index 30e91d2..fff08bd 100644
--- a/Sources/BinaryCodable/Encoding/AbstractEncodingNode.swift
+++ b/Sources/BinaryCodable/Encoding/AbstractEncodingNode.swift
@@ -1,7 +1,21 @@
import Foundation
+/**
+ A class that provides encoding functions for all encoding containers.
+ */
class AbstractEncodingNode: AbstractNode {
+ let needsLengthData: Bool
+
+ /**
+ - Parameter codingPath: The path to get to this point in encoding or decoding
+ - Parameter userInfo: Contextual information set by the user
+ */
+ init(needsLengthData: Bool, codingPath: [CodingKey], userInfo: UserInfo) {
+ self.needsLengthData = needsLengthData
+ super.init(codingPath: codingPath, userInfo: userInfo)
+ }
+
/**
Sort keyed data in the binary representation.
@@ -15,13 +29,16 @@ class AbstractEncodingNode: AbstractNode {
- Note: The default value for this option is `false`.
*/
var sortKeysDuringEncoding: Bool {
- userInfo.has(.sortKeys)
+ userInfo[BinaryEncoder.userInfoSortKey] as? Bool ?? false
}
-
- var containsOptional: Bool
- init(path: [CodingKey], info: UserInfo, optional: Bool) {
- self.containsOptional = optional
- super.init(path: path, info: info)
+ func encodeValue(_ value: T, needsLengthData: Bool) throws -> EncodableContainer where T : Encodable {
+ if T.self is EncodablePrimitive.Type, let base = value as? EncodablePrimitive {
+ return PrimitiveEncodingContainer(wrapped: base, needsLengthData: needsLengthData)
+ } else {
+ let encoder = EncodingNode(needsLengthData: needsLengthData, codingPath: codingPath, userInfo: userInfo)
+ try value.encode(to: encoder)
+ return encoder
+ }
}
}
diff --git a/Sources/BinaryCodable/Encoding/CodingKeyWrapper.swift b/Sources/BinaryCodable/Encoding/CodingKeyWrapper.swift
deleted file mode 100644
index 2b264e0..0000000
--- a/Sources/BinaryCodable/Encoding/CodingKeyWrapper.swift
+++ /dev/null
@@ -1,169 +0,0 @@
-import Foundation
-
-/**
- The largest value (inclusive) for a valid integer coding key.
-
- The value is an integer with all but the first 5 MSB set to `1`.
-
- On 64 bit systems, the value is:
-
- Hex: `0x07FFFFFFFFFFFFFF`
-
- Decimal: `576460752303423487`
-
- Mathmatically: `2^59-1`
-
- On 32 bit systems, the value is:
-
- Hex: `0x07FFFFFF`
-
- Decimal: `134217727`
-
- Mathmatically: `2^27-1`
- */
-private let intKeyUpperBound = Int(bitPattern: ~UInt(0) >> 5)
-
-/**
- The smallest value (inclusive) for a valid integer coding key.
-
- The value is an integer with only the first 5 MSB set to `1`.
-
- On 64 bit systems, the value is:
-
- Hex: `0xF800000000000000`
-
- Decimal: `-576460752303423488`
-
- Mathmatically: `-2^59`
-
- On 32 bit systems, the value is:
-
- Hex: `0xF8000000`
-
- Decimal: `-134217728`
-
- Mathmatically: `-2^27`
- */
-private let intKeyLowerBound = Int(bitPattern: 0xF8 << (UInt.bitWidth - 8))
-
-
-protocol CodingKeyWrapper {
-
- func encode(for dataType: DataType) -> Data
-
- var stringValue: String { get }
-}
-
-/**
- A wrapper around a coding key to allow usage in dictionaries.
- */
-struct MixedCodingKeyWrapper: CodingKeyWrapper {
-
- private let intValue: Int?
-
- let stringValue: String
-
- /**
- Create a wrapper around a coding key.
- */
- init(_ codingKey: CodingKey) {
- if let int = codingKey.intValue {
- // Check that integer key is in valid range
- if int > intKeyUpperBound || int < intKeyLowerBound {
- fatalError("Integer key \(int) is out of range [\(intKeyLowerBound)...\(intKeyUpperBound)] for coding key \(codingKey.stringValue)")
- }
- }
- self.intValue = codingKey.intValue
- self.stringValue = codingKey.stringValue
- }
-
- init(intValue: Int?, stringValue: String) {
- self.intValue = intValue
- self.stringValue = stringValue
- }
-
- /**
- Encode a coding key for a value of a specific type.
- - Parameter dataType: The data type of the value associated with this key.
- - Returns: The encoded data of the coding key, the data type and the key type indicator.
- */
- func encode(for dataType: DataType) -> Data {
- guard let intValue = intValue else {
- return encodeStringValue(for: dataType)
- }
- return encode(int: intValue, for: dataType, isStringKey: false)
- }
-
- /**
- Encode the string value of the coding key.
- - Parameter dataType: The data type of the value associated with this key.
- - Returns: The encoded data of the coding key, the data type and the key type indicator.
- */
- private func encodeStringValue(for dataType: DataType) -> Data {
- let count = stringValue.count
- let data = encode(int: count, for: dataType, isStringKey: true)
- // It's assumed that all property names can be represented in UTF-8
- return data + stringValue.data(using: .utf8)!
- }
-
- /**
- Encode an integer value with a data type and the string key indicator.
-
- - Note: The integer is encoded using variable-length encoding.
- - Note: Integer range is checked in constructor. It's also assumed that a property name will not exceed 2^59 characters.
- - Parameter int: The integer (either string key length or integer key) to encode.
- - Parameter dataType: The data type of the value associated with this key.
- - Returns: The encoded data of the integer, the data type and the key type indicator.
- */
- private func encode(int: Int, for dataType: DataType, isStringKey: Bool) -> Data {
- // Bit 0-2 are the data type
- // Bit 3 is the string key indicator
- // Remaining bits are the integer
- let mixed = (int << 4) | dataType.rawValue | (isStringKey ? 0x08 : 0x00)
- return mixed.variableLengthEncoding
- }
-}
-
-extension MixedCodingKeyWrapper: Equatable {
-
- static func == (lhs: MixedCodingKeyWrapper, rhs: MixedCodingKeyWrapper) -> Bool {
- lhs.stringValue == rhs.stringValue
- }
-}
-
-extension MixedCodingKeyWrapper: Hashable {
-
- func hash(into hasher: inout Hasher) {
- if let int = intValue {
- hasher.combine(int)
- } else {
- hasher.combine(stringValue)
- }
- }
-}
-
-extension MixedCodingKeyWrapper: Comparable {
-
- static func < (lhs: MixedCodingKeyWrapper, rhs: MixedCodingKeyWrapper) -> Bool {
- if let lhsInt = lhs.intValue, let rhsInt = rhs.intValue {
- return lhsInt < rhsInt
- }
- return lhs.stringValue < rhs.stringValue
- }
-}
-
-extension MixedCodingKeyWrapper: EncodingContainer {
-
- var data: Data {
- if let intValue = intValue {
- return UInt64(intValue).protobufData()
- }
- return stringValue.data(using: .utf8)!
- }
-
- var dataType: DataType {
- intValue != nil ? Int.dataType: String.dataType
- }
-
- var isEmpty: Bool { false }
-}
diff --git a/Sources/BinaryCodable/Encoding/CodingOption.swift b/Sources/BinaryCodable/Encoding/CodingOption.swift
deleted file mode 100644
index 8ab8f6c..0000000
--- a/Sources/BinaryCodable/Encoding/CodingOption.swift
+++ /dev/null
@@ -1,19 +0,0 @@
-import Foundation
-
-enum CodingOption: String {
-
- case sortKeys = "sort"
-
- case prependNilIndicesForUnkeyedContainers = "nilIndices"
-
- var infoKey: CodingUserInfoKey {
- .init(rawValue: rawValue)!
- }
-}
-
-extension UserInfo {
-
- func has(_ option: CodingOption) -> Bool {
- (self[option.infoKey] as? Bool) == true
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/EncodableContainer.swift b/Sources/BinaryCodable/Encoding/EncodableContainer.swift
new file mode 100644
index 0000000..5fa668d
--- /dev/null
+++ b/Sources/BinaryCodable/Encoding/EncodableContainer.swift
@@ -0,0 +1,89 @@
+import Foundation
+
+/**
+ A protocol adopted by primitive types for encoding.
+ */
+protocol EncodableContainer {
+
+ /// Indicate if the container needs to have a length prepended
+ var needsLengthData: Bool { get }
+
+ /// Indicate if the container can encode nil
+ var needsNilIndicator: Bool { get }
+
+ /// Indicate if the container encodes nil
+ /// - Note: This property must not be `true` if `needsNilIndicator` is set to `false`
+ var isNil: Bool { get }
+
+ /**
+ Provide the data encoded in the container
+ - Note: No length information must be included
+ - Note: This function is only called if `isNil` is false
+ */
+ func containedData() throws -> Data
+}
+
+extension EncodableContainer {
+
+ /**
+
+ */
+ func completeData(with key: CodingKey, codingPath: [CodingKey]) throws -> Data {
+ try key.keyData(codingPath: codingPath) + completeData()
+ }
+
+ /**
+ The full data encoded in the container, including nil indicator and length, if needed
+ */
+ func completeData() throws -> Data {
+ guard !isNil else {
+ // A nil value always means:
+ // - That the length is zero
+ // - That a nil indicator is needed
+ return Data([0x01])
+ }
+ let data = try containedData()
+ if needsLengthData {
+ // It doesn't matter if `needsNilIndicator` is true or false
+ // Length always includes it
+ return data.count.lengthData + data
+ }
+ if needsNilIndicator {
+ return Data([0x00]) + data
+ }
+ return data
+ }
+}
+
+private extension Int {
+
+ /// Encodes the integer as the length of a nil/length indicator
+ var lengthData: Data {
+ // The first bit (LSB) is the `nil` bit (0)
+ // The rest is the length, encoded as a varint
+ (UInt64(self) << 1).encodedData
+ }
+}
+
+private extension CodingKey {
+
+ func keyData(codingPath: [CodingKey]) throws -> Data {
+ // String or Int key bit
+ // Length of String key or Int key as varint
+ // String Key Data
+ guard let intValue else {
+ let stringData = stringValue.data(using: .utf8)!
+ // Set String bit to 1
+ let lengthValue = (UInt64(stringData.count) << 1) + 0x01
+ let lengthData = lengthValue.encodedData
+ return lengthData + stringData
+ }
+ guard intValue >= 0 else {
+ throw EncodingError.invalidValue(intValue, .init(codingPath: codingPath + [self], debugDescription: "Invalid integer value for coding key"))
+ }
+ // For integer keys:
+ // The LSB is set to 0
+ // Encode 2 * intValue
+ return intValue.lengthData
+ }
+}
diff --git a/Sources/BinaryCodable/Encoding/EncodedPrimitive.swift b/Sources/BinaryCodable/Encoding/EncodedPrimitive.swift
deleted file mode 100644
index 665cc44..0000000
--- a/Sources/BinaryCodable/Encoding/EncodedPrimitive.swift
+++ /dev/null
@@ -1,33 +0,0 @@
-import Foundation
-
-struct EncodedPrimitive: EncodingContainer {
-
- let dataType: DataType
-
- let data: Data
-
- let isEmpty: Bool
-
- init(primitive: EncodablePrimitive) throws {
- self.dataType = primitive.dataType
- self.data = try primitive.data()
- self.isEmpty = false
- }
-
- init(protobuf: EncodablePrimitive, excludeDefaults: Bool = false) throws {
- guard protobuf.dataType.isProtobufCompatible else {
- throw ProtobufEncodingError.unsupported(type: protobuf)
- }
- guard let value = protobuf as? ProtobufEncodable else {
- throw ProtobufEncodingError.unsupported(type: protobuf)
- }
- if excludeDefaults && value.isZero {
- self.data = .empty
- self.isEmpty = true
- } else {
- self.data = try value.protobufData()
- self.isEmpty = false
- }
- self.dataType = protobuf.dataType
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/EncodingContainer.swift b/Sources/BinaryCodable/Encoding/EncodingContainer.swift
deleted file mode 100644
index bdbb742..0000000
--- a/Sources/BinaryCodable/Encoding/EncodingContainer.swift
+++ /dev/null
@@ -1,34 +0,0 @@
-import Foundation
-
-protocol EncodingContainer {
-
- var data: Data { get }
-
- var dataType: DataType { get }
-
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data
-
- var isEmpty: Bool { get }
-
- var dataWithLengthInformationIfRequired: Data { get }
-}
-
-extension EncodingContainer {
-
- var dataWithLengthInformationIfRequired: Data {
- guard dataType == .variableLength else {
- return data
- }
- return dataWithLengthInformation
- }
-
- var dataWithLengthInformation: Data {
- let data = self.data
- return data.count.variableLengthEncoding + data
- }
-
-
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data {
- key.encode(for: dataType) + dataWithLengthInformationIfRequired
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/EncodingNode.swift b/Sources/BinaryCodable/Encoding/EncodingNode.swift
index 944f395..3bbce37 100644
--- a/Sources/BinaryCodable/Encoding/EncodingNode.swift
+++ b/Sources/BinaryCodable/Encoding/EncodingNode.swift
@@ -1,61 +1,54 @@
import Foundation
-class EncodingNode: AbstractEncodingNode, Encoder {
-
- var container: EncodingContainer?
-
- func wrap(container: () -> T) -> T where T: EncodingContainer {
- guard self.container == nil else {
- fatalError("Multiple calls to `container<>(keyedBy:)`, `unkeyedContainer()`, or `singleValueContainer()` for an encoder")
+final class EncodingNode: AbstractEncodingNode, Encoder {
+
+ private var hasMultipleCalls = false
+
+ private var encodedValue: EncodableContainer? = nil
+
+ private func assign(_ value: T) -> T where T: EncodableContainer {
+ // Prevent multiple calls to container(keyedBy:), unkeyedContainer(), or singleValueContainer()
+ if encodedValue == nil {
+ encodedValue = value
+ } else {
+ hasMultipleCalls = true
}
- let value = container()
- self.container = value
return value
}
-
+
func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey {
- let container = wrap { KeyedEncoder(path: codingPath, info: userInfo, optional: containsOptional) }
- return KeyedEncodingContainer(container)
+ return KeyedEncodingContainer(assign(KeyedEncoder(needsLengthData: needsLengthData, codingPath: codingPath, userInfo: userInfo)))
}
-
+
func unkeyedContainer() -> UnkeyedEncodingContainer {
- wrap { UnkeyedEncoder(path: codingPath, info: userInfo, optional: containsOptional) }
- }
-
- func singleValueContainer() -> SingleValueEncodingContainer {
- wrap { ValueEncoder(path: codingPath, info: userInfo, optional: containsOptional) }
+ return assign(UnkeyedEncoder(needsLengthData: needsLengthData, codingPath: codingPath, userInfo: userInfo))
}
- func encoding(_ value: T) throws -> EncodingNode where T: Encodable {
- try value.encode(to: self)
- return self
+ func singleValueContainer() -> SingleValueEncodingContainer {
+ return assign(ValueEncoder(codingPath: codingPath, userInfo: userInfo))
}
}
-extension EncodingNode: EncodingContainer {
+extension EncodingNode: EncodableContainer {
- var data: Data {
- container!.data
+ var needsNilIndicator: Bool {
+ // If no value is encoded, then it doesn't matter what is returned, `encodedData()` will throw an error
+ encodedValue?.needsNilIndicator ?? false
}
- var dataWithLengthInformationIfRequired: Data {
- container!.dataWithLengthInformationIfRequired
- }
-
- var dataType: DataType {
- container!.dataType
+ var isNil: Bool {
+ // Returning false for missing encodedValue forces an error on `encodedData()`
+ encodedValue?.isNil ?? false
}
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data {
- guard let container else {
- // Explicitly check for nil to prevent error with unwrapping
- // when not using `encodeNil(forKey:)` for custom encoders
- return Data()
+ func containedData() throws -> Data {
+ guard !hasMultipleCalls else {
+ throw EncodingError.invalidValue(0, .init(codingPath: codingPath, debugDescription: "Multiple calls to container(keyedBy:), unkeyedContainer(), or singleValueContainer()"))
}
- return container.encodeWithKey(key)
- }
-
- var isEmpty: Bool {
- container?.isEmpty ?? true
+ guard let encodedValue else {
+ throw EncodingError.invalidValue(0, .init(codingPath: codingPath, debugDescription: "No calls to container(keyedBy:), unkeyedContainer(), or singleValueContainer()"))
+ }
+ let data = try encodedValue.containedData()
+ return data
}
}
diff --git a/Sources/BinaryCodable/Encoding/HashableKey.swift b/Sources/BinaryCodable/Encoding/HashableKey.swift
new file mode 100644
index 0000000..6bb6b68
--- /dev/null
+++ b/Sources/BinaryCodable/Encoding/HashableKey.swift
@@ -0,0 +1,34 @@
+import Foundation
+
+/**
+ A wrapper for a coding key to use as a dictionary key
+ */
+struct HashableKey {
+
+ let key: CodingKey
+}
+
+extension HashableKey: Equatable {
+
+ static func == (lhs: HashableKey, rhs: HashableKey) -> Bool {
+ lhs.key.stringValue == rhs.key.stringValue
+ }
+}
+
+extension HashableKey: Hashable {
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(key.stringValue)
+ }
+}
+
+extension HashableKey: Comparable {
+
+ static func < (lhs: HashableKey, rhs: HashableKey) -> Bool {
+ guard let lhsInt = lhs.key.intValue,
+ let rhsInt = rhs.key.intValue else {
+ return lhs.key.stringValue < rhs.key.stringValue
+ }
+ return lhsInt < rhsInt
+ }
+}
diff --git a/Sources/BinaryCodable/Encoding/KeyedEncoder.swift b/Sources/BinaryCodable/Encoding/KeyedEncoder.swift
index 1cfc0db..8fb460b 100644
--- a/Sources/BinaryCodable/Encoding/KeyedEncoder.swift
+++ b/Sources/BinaryCodable/Encoding/KeyedEncoder.swift
@@ -1,78 +1,86 @@
import Foundation
final class KeyedEncoder: AbstractEncodingNode, KeyedEncodingContainerProtocol where Key: CodingKey {
-
- var content = [MixedCodingKeyWrapper : EncodingContainer]()
- override init(path: [CodingKey], info: UserInfo, optional: Bool) {
- super.init(path: path, info: info, optional: optional)
- }
+ private var encodedValues: [HashableKey : EncodableContainer] = [:]
- func assign(_ value: EncodingContainer, to key: CodingKey) {
- let wrapped = MixedCodingKeyWrapper(key)
- content[wrapped] = value
- }
-
- func encodeNil(forKey key: Key) throws {
- // Nothing to do, nil is ommited for keyed containers
- }
-
- func encode(_ value: T, forKey key: Key) throws where T : Encodable {
- let container: EncodingContainer
- if value is AnyOptional {
- container = try EncodingNode(path: codingPath, info: userInfo, optional: true).encoding(value)
- } else if let primitive = value as? EncodablePrimitive {
- container = try wrapError(path: codingPath) { try EncodedPrimitive(primitive: primitive) }
+ /// Internal indicator to prevent assigning a single key multiple times
+ private var multiplyAssignedKey: HashableKey? = nil
+
+ @discardableResult
+ private func assign(_ value: T, forKey key: CodingKey) -> T where T: EncodableContainer {
+ let hashableKey = HashableKey(key: key)
+ if encodedValues[hashableKey] != nil {
+ multiplyAssignedKey = hashableKey
} else {
- let node = EncodingNode(
- path: codingPath,
- info: userInfo,
- optional: false)
- container = try node.encoding(value)
+ encodedValues[hashableKey] = value
}
- assign(container, to: key)
+ return value
}
-
+
+ private func assignedNode(forKey key: CodingKey) -> EncodingNode {
+ let node = EncodingNode(needsLengthData: true, codingPath: codingPath + [key], userInfo: userInfo)
+ return assign(node, forKey: key)
+ }
+
func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = KeyedEncoder(path: codingPath + [key], info: userInfo, optional: false)
- assign(container, to: key)
- return KeyedEncodingContainer(container)
+ // By wrapping the nested container in a node, it adds length information to it
+ KeyedEncodingContainer(assignedNode(forKey: key).container(keyedBy: keyType))
}
-
+
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
- let container = UnkeyedEncoder(path: codingPath + [key], info: userInfo, optional: false)
- assign(container, to: key)
- return container
+ // By wrapping the nested container in a node, it adds length information to it
+ assignedNode(forKey: key).unkeyedContainer()
}
-
+
func superEncoder() -> Encoder {
- let container = EncodingNode(path: codingPath, info: userInfo, optional: false)
- assign(container, to: SuperEncoderKey())
- return container
+ assignedNode(forKey: SuperCodingKey())
}
-
+
func superEncoder(forKey key: Key) -> Encoder {
- let container = EncodingNode(path: codingPath + [key], info: userInfo, optional: false)
- assign(container, to: key)
- return container
+ assignedNode(forKey: key)
+ }
+
+ func encodeNil(forKey key: Key) throws {
+ // If a value is nil, then it is not encoded
+ // This is not consistent with the documentation of `decodeNil(forKey:)`,
+ // which states that when decodeNil() should fail if the key is not present.
+ // We could fix this by explicitly assigning a `nil` value:
+ // `assign(NilContainer(), forKey: key)`
+ // But this would cause other problems, either breaking the decoding of double optionals (e.g. Int??),
+ // Or by requiring an additional `nil` indicator for ALL values in keyed containers,
+ // which would make the format a lot less efficient
+ }
+
+ func encode(_ value: T, forKey key: Key) throws where T : Encodable {
+ let encoded = try encodeValue(value, needsLengthData: true)
+ assign(encoded, forKey: key)
}
}
+extension KeyedEncoder: EncodableContainer {
-extension KeyedEncoder: EncodingContainer {
+ var needsNilIndicator: Bool {
+ false
+ }
- var data: Data {
- if sortKeysDuringEncoding {
- return content.sorted { $0.key < $1.key }.map { $1.encodeWithKey($0) }.reduce(Data(), +)
- }
- return content.map { $1.encodeWithKey($0) }.reduce(Data(), +)
+ var isNil: Bool {
+ false
}
-
- var dataType: DataType {
- .variableLength
+
+ func containedData() throws -> Data {
+ if let multiplyAssignedKey {
+ throw EncodingError.invalidValue(0, .init(codingPath: codingPath, debugDescription: "Multiple values assigned to key \(multiplyAssignedKey)"))
+ }
+ guard sortKeysDuringEncoding else {
+ return try encode(elements: encodedValues)
+ }
+ return try encode(elements: encodedValues.sorted { $0.key < $1.key })
}
- var isEmpty: Bool {
- !content.values.contains { !$0.isEmpty }
+ private func encode(elements: T) throws -> Data where T: Collection, T.Element == (key: HashableKey, value: EncodableContainer) {
+ try elements.map { key, value in
+ try value.completeData(with: key.key, codingPath: codingPath)
+ }.joinedData
}
}
diff --git a/Sources/BinaryCodable/Encoding/NilContainer.swift b/Sources/BinaryCodable/Encoding/NilContainer.swift
new file mode 100644
index 0000000..8dda6d9
--- /dev/null
+++ b/Sources/BinaryCodable/Encoding/NilContainer.swift
@@ -0,0 +1,17 @@
+import Foundation
+
+/**
+ A container to signal that `nil` was encoded in a container
+ */
+struct NilContainer: EncodableContainer {
+
+ let needsLengthData = true
+
+ let needsNilIndicator = true
+
+ let isNil = true
+
+ func containedData() throws -> Data {
+ fatalError()
+ }
+}
diff --git a/Sources/BinaryCodable/Encoding/PrimitiveEncodingContainer.swift b/Sources/BinaryCodable/Encoding/PrimitiveEncodingContainer.swift
new file mode 100644
index 0000000..c4bd4db
--- /dev/null
+++ b/Sources/BinaryCodable/Encoding/PrimitiveEncodingContainer.swift
@@ -0,0 +1,21 @@
+import Foundation
+
+struct PrimitiveEncodingContainer: EncodableContainer {
+
+ let needsLengthData: Bool
+
+ let wrapped: EncodablePrimitive
+
+ let needsNilIndicator = false
+
+ let isNil = false
+
+ init(wrapped: EncodablePrimitive, needsLengthData: Bool) {
+ self.needsLengthData = needsLengthData
+ self.wrapped = wrapped
+ }
+
+ func containedData() throws -> Data {
+ wrapped.encodedData
+ }
+}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/AbstractProtoNode.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/AbstractProtoNode.swift
deleted file mode 100644
index 2dfd0fe..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/AbstractProtoNode.swift
+++ /dev/null
@@ -1,22 +0,0 @@
-import Foundation
-
-class AbstractProtoNode {
-
- let codingPath: [CodingKey]
-
- let userInfo: [CodingUserInfoKey : Any]
-
- let encodedType: String
-
- var encodingError: Error?
-
- var isRoot: Bool {
- codingPath.isEmpty
- }
-
- init(encoding encodedType: String, path: [CodingKey], info: [CodingUserInfoKey : Any]) {
- self.encodedType = encodedType
- self.codingPath = path
- self.userInfo = info
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/KeyedProtoEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/KeyedProtoEncoder.swift
deleted file mode 100644
index 35a51b1..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/KeyedProtoEncoder.swift
+++ /dev/null
@@ -1,92 +0,0 @@
-import Foundation
-
-final class KeyedProtoEncoder: AbstractProtoNode, KeyedEncodingContainerProtocol where Key: CodingKey {
-
- var content = [ProtoKeyWrapper : ProtoContainer]()
-
- @discardableResult
- func assign(to key: CodingKey, container: () throws -> T) rethrows -> T where T: ProtoContainer {
- let wrapped = ProtoKeyWrapper(key)
- guard content[wrapped] == nil else {
- fatalError("Multiple values encoded for key \(key)")
- }
- let value = try container()
- content[wrapped] = value
- return value
- }
-
- func encodeNil(forKey key: Key) throws {
- throw ProtobufEncodingError.nilValuesNotSupported
- }
-
- func encode(_ value: T, forKey key: Key) throws where T : Encodable {
- if value is ProtobufOneOf {
- // TODO: Create oneof protobuf definition
- throw ProtobufEncodingError.unsupportedType("Oneof definition")
- }
- if let primitive = value as? EncodablePrimitive {
- guard let protoPrimitive = primitive as? ProtobufEncodable else {
- throw ProtobufEncodingError.unsupported(type: primitive)
- }
- try assign(to: key) {
- try ProtoPrimitive(primitive: protoPrimitive)
- }
- return
- }
- try assign(to: key) {
- try ProtoNode(encoding: "\(type(of: value))", path: codingPath, info: userInfo)
- .encoding(value)
- }
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = assign(to: key) {
- KeyedProtoEncoder(encoding: encodedType, path: codingPath + [key], info: userInfo)
- }
- return KeyedEncodingContainer(container)
- }
-
- func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
- assign(to: key) {
- UnkeyedProtoEncoder(encoding: encodedType, path: codingPath + [key], info: userInfo)
- }
- }
-
- func superEncoder() -> Encoder {
- fatalError("Protobuf compatibility does not support encoding super")
- }
-
- func superEncoder(forKey key: Key) -> Encoder {
- fatalError("Protobuf compatibility does not support encoding super")
- }
-}
-
-
-extension KeyedProtoEncoder: ProtoContainer {
-
- func protobufDefinition() throws -> String {
-
- let prefix = "message \(encodedType) {\n\n"
-
- let fields = try content
- .sorted { $0.key < $1.key }
- .map { key, value -> String in
- guard let field = key.intValue else {
- throw ProtobufEncodingError.missingIntegerKey(key.stringValue)
- }
- // TODO: Add additional message definitions
- // The protobuf description needs to print also nested messages
- // Currently, only the top level is shown
- // This requires additional methods on the hierarchy to get all nested definitions
- // within a container.
- return "\(value.protoTypeName) \(key.stringValue) = \(field);"
- }
- .joined(separator: "\n\n").indented()
- let suffix = "\n}"
- return prefix + fields + suffix
- }
-
- var protoTypeName: String {
- encodedType
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoContainer.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoContainer.swift
deleted file mode 100644
index 790db57..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoContainer.swift
+++ /dev/null
@@ -1,8 +0,0 @@
-import Foundation
-
-protocol ProtoContainer {
-
- func protobufDefinition() throws -> String
-
- var protoTypeName: String { get }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoKeyWrapper.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoKeyWrapper.swift
deleted file mode 100644
index cd1aaaa..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoKeyWrapper.swift
+++ /dev/null
@@ -1,40 +0,0 @@
-import Foundation
-
-/**
- A wrapper around a coding key to allow usage in dictionaries.
- */
-struct ProtoKeyWrapper {
-
- let intValue: Int?
-
- let stringValue: String
-
- /**
- Create a wrapper around a coding key.
- */
- init(_ codingKey: CodingKey) {
- self.intValue = codingKey.intValue
- self.stringValue = codingKey.stringValue
- }
-}
-
-extension ProtoKeyWrapper: Equatable {
-
- static func == (lhs: ProtoKeyWrapper, rhs: ProtoKeyWrapper) -> Bool {
- lhs.intValue == rhs.intValue
- }
-}
-
-extension ProtoKeyWrapper: Hashable {
-
-}
-
-extension ProtoKeyWrapper: Comparable {
-
- static func < (lhs: ProtoKeyWrapper, rhs: ProtoKeyWrapper) -> Bool {
- guard let l = lhs.intValue, let r = rhs.intValue else {
- return lhs.stringValue < rhs.stringValue
- }
- return l < r
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoNode.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoNode.swift
deleted file mode 100644
index 276f432..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoNode.swift
+++ /dev/null
@@ -1,51 +0,0 @@
-import Foundation
-
-final class ProtoNode: AbstractProtoNode, Encoder {
-
- var container: ProtoContainer?
-
- private func wrap(container: () -> T) -> T where T: ProtoContainer {
- let value = container()
- guard self.container == nil else {
- encodingError = ProtobufEncodingError.multipleContainersAccessed
- return value
- }
- self.container = value
- return value
- }
-
- func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey {
- let container = wrap { KeyedProtoEncoder(encoding: encodedType, path: codingPath, info: userInfo) }
- return KeyedEncodingContainer(container)
- }
-
- func unkeyedContainer() -> UnkeyedEncodingContainer {
- wrap { UnkeyedProtoEncoder(encoding: encodedType, path: codingPath, info: userInfo) }
- }
-
- func singleValueContainer() -> SingleValueEncodingContainer {
- wrap { ValueProtoEncoder(encoding: encodedType, path: codingPath, info: userInfo) }
- }
-
- func encoding(_ value: T) throws -> ProtoNode where T: Encodable {
- try value.encode(to: self)
- return self
- }
-}
-
-extension ProtoNode: ProtoContainer {
-
- func protobufDefinition() throws -> String {
- if let error = encodingError {
- throw error
- }
- guard let container = container else {
- throw ProtobufEncodingError.noContainersAccessed
- }
- return try container.protobufDefinition()
- }
-
- var protoTypeName: String {
- container!.protoTypeName
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoPrimitive.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoPrimitive.swift
deleted file mode 100644
index e125554..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ProtoPrimitive.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-import Foundation
-
-struct ProtoPrimitive: ProtoContainer {
-
- let protoTypeName: String
-
- init(primitive: ProtobufEncodable) throws {
- self.protoTypeName = primitive.protoType
- }
-
- func protobufDefinition() throws -> String {
- ""
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/UnkeyedProtoEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/UnkeyedProtoEncoder.swift
deleted file mode 100644
index fcc0e93..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/UnkeyedProtoEncoder.swift
+++ /dev/null
@@ -1,77 +0,0 @@
-import Foundation
-
-final class UnkeyedProtoEncoder: AbstractProtoNode, UnkeyedEncodingContainer {
-
- var count: Int {
- content.count
- }
-
- private var content = [ProtoContainer]()
-
- @discardableResult
- private func assign(_ encoded: () throws -> T) rethrows -> T where T: ProtoContainer {
- let value = try encoded()
- if let existingType = content.first?.protoTypeName, existingType != value.protoTypeName {
- encodingError = ProtobufEncodingError.multipleTypesInUnkeyedContainer
- }
- content.append(value)
- return value
- }
-
- func encodeNil() throws {
- throw ProtobufEncodingError.nilValuesNotSupported
- }
-
- func encode(_ value: T) throws where T : Encodable {
- if let primitive = value as? EncodablePrimitive {
- guard let protoPrimitive = primitive as? ProtobufEncodable else {
- throw ProtobufEncodingError.unsupported(type: primitive)
- }
- try assign {
- try ProtoPrimitive(primitive: protoPrimitive)
- }
- return
- }
- try assign {
- try ProtoNode(encoding: "\(type(of: value))", path: codingPath, info: userInfo)
- .encoding(value)
- }
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = assign {
- KeyedProtoEncoder(encoding: encodedType, path: codingPath, info: userInfo)
- }
- return KeyedEncodingContainer(container)
- }
-
- func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
- assign {
- UnkeyedProtoEncoder(encoding: encodedType, path: codingPath, info: userInfo)
- }
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(error: .superNotSupported, path: codingPath, info: userInfo)
- }
-}
-
-extension UnkeyedProtoEncoder: ProtoContainer {
-
- func protobufDefinition() throws -> String {
- if isRoot {
- throw ProtobufEncodingError.rootIsNotKeyedContainer
- }
- if let error = encodingError {
- throw error
- }
- guard let def = try content.first?.protobufDefinition() else {
- throw ProtobufEncodingError.protobufDefinitionUnavailable("No value in unkeyed container to determine type")
- }
- return def
- }
-
- var protoTypeName: String {
- "repeated " + (content.first?.protoTypeName ?? "")
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ValueProtoEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ValueProtoEncoder.swift
deleted file mode 100644
index c46d4e5..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/Definitions/ValueProtoEncoder.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-import Foundation
-
-final class ValueProtoEncoder: AbstractProtoNode, SingleValueEncodingContainer {
-
- private var container: ProtoContainer?
-
- func encodeNil() throws {
- throw ProtobufEncodingError.nilValuesNotSupported
- }
-
- private func assign(_ encoded: () throws -> ProtoContainer?) rethrows {
- guard container == nil else {
- encodingError = ProtobufEncodingError.multipleValuesInSingleValueContainer
- return
- }
- container = try encoded()
- }
-
- func encode(_ value: T) throws where T : Encodable {
- if let primitive = value as? EncodablePrimitive {
- guard let protoPrimitive = primitive as? ProtobufEncodable else {
- throw ProtobufEncodingError.unsupported(type: primitive)
- }
- try assign {
- try ProtoPrimitive(primitive: protoPrimitive)
- }
- return
- }
- try assign {
- try ProtoNode(encoding: "\(type(of: value))", path: codingPath, info: userInfo).encoding(value)
- }
- }
-}
-
-extension ValueProtoEncoder: ProtoContainer {
-
- func protobufDefinition() throws -> String {
- if isRoot {
- throw ProtobufEncodingError.rootIsNotKeyedContainer
- }
- guard let container = container else {
- throw ProtobufEncodingError.noValueInSingleValueContainer
- }
- return try container.protobufDefinition()
- }
-
- var protoTypeName: String {
- container?.protoTypeName ?? "No container"
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/IntKeyWrapper.swift b/Sources/BinaryCodable/Encoding/Protobuf/IntKeyWrapper.swift
deleted file mode 100644
index 197675d..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/IntKeyWrapper.swift
+++ /dev/null
@@ -1,73 +0,0 @@
-import Foundation
-
-/// The largest value (inclusive) for a valid protobuf field number (536870911)
-private let protoFieldUpperBound = 0x1FFFFFFF
-
-/// The smallest value (inclusive) for a valid integer coding key
-private let protoFieldLowerBound = 1
-
-/**
- Wraps a protobuf field number to use as a coding key.
- */
-struct IntKeyWrapper: CodingKeyWrapper {
-
- private let intValue: Int
-
- var stringValue: String { "\(intValue)" }
-
- static func checkFieldBounds(_ field: Int) throws {
- if field < protoFieldLowerBound || field > protoFieldUpperBound {
- throw ProtobufEncodingError.integerKeyOutOfRange(field)
- }
- }
-
- init(value: Int) throws {
- try IntKeyWrapper.checkFieldBounds(value)
- self.intValue = value
- }
-
- init(_ key: CodingKey) throws {
- guard let value = key.intValue else {
- throw ProtobufEncodingError.missingIntegerKey(key.stringValue)
- }
- try self.init(value: value)
- }
-
- func encode(for dataType: DataType) -> Data {
- // Bit 0-2 are the data type
- // Remaining bits are the integer
- let mixed = (intValue << 3) | dataType.rawValue
- return mixed.variableLengthEncoding
- }
-}
-
-extension IntKeyWrapper: EncodingContainer {
-
- var data: Data {
- UInt64(intValue).protobufData()
- }
-
- var dataType: DataType {
- Int.dataType
- }
-
- var isEmpty: Bool {
- false
- }
-}
-
-
-extension IntKeyWrapper: Hashable {
-
-}
-
-extension IntKeyWrapper: Equatable {
-
-}
-
-extension IntKeyWrapper: Comparable {
-
- static func < (lhs: IntKeyWrapper, rhs: IntKeyWrapper) -> Bool {
- lhs.intValue < rhs.intValue
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfAssociatedValuesEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfAssociatedValuesEncoder.swift
deleted file mode 100644
index 4b729af..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfAssociatedValuesEncoder.swift
+++ /dev/null
@@ -1,80 +0,0 @@
-import Foundation
-
-/**
- A protocol to abstract a `OneOfAssociatedValuesEncoder`, which can't be directly stored due to its associated type.
- */
-protocol OneOfAssociatedValuesContainer {
-
- func getValue() throws -> EncodingContainer
-}
-
-/**
- A container to specifically encode the associated value of a `ProtobufOneOf` enum case.
-
- The container ignores the associated values key `_0` and only encodes the value.
-
- The container only allows a single call to `encode(_,forKey:)`, all other access will fail.
- */
-final class OneOfAssociatedValuesEncoder: AbstractEncodingNode, KeyedEncodingContainerProtocol where Key: CodingKey {
-
- /// The encoded associated value
- var value: EncodingContainer?
-
- func encodeNil(forKey key: Key) throws {
- throw ProtobufEncodingError.invalidAccess(
- "Unexpected call to `encodeNil(forKey:)` while encoding a `ProtobufOneOf` associated value")
- }
-
- func encode(_ value: T, forKey key: Key) throws where T : Encodable {
- guard self.value == nil else {
- throw ProtobufEncodingError.invalidAccess(
- "Multiple calls to `encode(_,forKey:)` while encoding a `ProtobufOneOf` associated value")
- }
-
- if let primitive = value as? EncodablePrimitive {
- self.value = try wrapError(path: codingPath) {
- try EncodedPrimitive(protobuf: primitive, excludeDefaults: false)
- }
- } else if value is AnyDictionary {
- self.value = try ProtoDictEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- } else {
- self.value = try ProtoEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- }
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = ProtoKeyedThrowingEncoder(
- error: .invalidAccess("Unexpected call to `nestedContainer(keyedBy:forKey:)` while encoding a `ProtobufOneOf` associated value"),
- path: codingPath + [key], info: userInfo)
- return KeyedEncodingContainer(container)
- }
-
- func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
- let container = ProtoUnkeyedThrowingEncoder(
- error: .invalidAccess("Unexpected call to `nestedUnkeyedContainer(forKey:)` while encoding a `ProtobufOneOf` associated value"),
- path: codingPath + [key], info: userInfo)
- return container
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(
- error: .invalidAccess("Unexpected call to `superEncoder()` while encoding a `ProtobufOneOf` associated value"),
- path: codingPath, info: userInfo)
- }
-
- func superEncoder(forKey key: Key) -> Encoder {
- ProtoThrowingNode(
- error: .invalidAccess("Unexpected call to `superEncoder(forKey:)` while encoding a `ProtobufOneOf` associated value"),
- path: codingPath, info: userInfo)
- }
-}
-
-extension OneOfAssociatedValuesEncoder: OneOfAssociatedValuesContainer {
-
- func getValue() throws -> EncodingContainer {
- guard let value = value else {
- throw ProtobufEncodingError.noContainersAccessed
- }
- return value
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfEncodingNode.swift b/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfEncodingNode.swift
deleted file mode 100644
index ab711bb..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfEncodingNode.swift
+++ /dev/null
@@ -1,43 +0,0 @@
-import Foundation
-
-/**
- An encoding node specifically to encode `ProtobufOneOf` types, which require adjustments to the encoding process.
-
- The node wraps a `OneOfKeyedEncoder`, which encodes the enum case and associated value.
-
- The node allows only one call to `container(keyedBy:)`, all other access will fail.
- */
-final class OneOfEncodingNode: AbstractEncodingNode, Encoder {
-
- /// The wrapped container encoding the enum case and associated value
- var container: OneOfKeyedContainer?
-
- func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey {
- guard self.container == nil else {
- fatalError("Multiple calls to `container<>(keyedBy:)` while encoding a `ProtobufOneOf` type")
- }
- let container = OneOfKeyedEncoder(path: codingPath, info: userInfo, optional: false)
- self.container = container
- return KeyedEncodingContainer(container)
- }
-
- func unkeyedContainer() -> UnkeyedEncodingContainer {
- ProtoUnkeyedThrowingEncoder(
- error: .invalidAccess("Unexpected call to `unkeyedContainer()` while encoding a `ProtobufOneOf` type"),
- path: codingPath, info: userInfo)
- }
-
- func singleValueContainer() -> SingleValueEncodingContainer {
- ProtoValueThrowingEncoder(
- error: .invalidAccess("Unexpected call to `singleValueContainer()` while encoding a `ProtobufOneOf` type"),
- path: codingPath, info: userInfo)
- }
-
- func encoding(_ value: T) throws -> (key: IntKeyWrapper, value: EncodingContainer) where T: Encodable {
- try value.encode(to: self)
- guard let container = container else {
- throw ProtobufEncodingError.noContainersAccessed
- }
- return try container.getContent()
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfKeyedEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfKeyedEncoder.swift
deleted file mode 100644
index 0ed91ca..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/OneOf/OneOfKeyedEncoder.swift
+++ /dev/null
@@ -1,74 +0,0 @@
-import Foundation
-
-/**
- A protocol to abstract a `OneOfKeyedEncoder`, which can't be directly stored due to the associated type.
- */
-protocol OneOfKeyedContainer {
-
- func getContent() throws -> (key: IntKeyWrapper, value: EncodingContainer)
-}
-
-/**
- A keyed encoder specifically to encode a `ProtobufOneOf` enum case and associated value.
-
- The encoder is wrapped by `OneOfEncodingNoder`, and itself wraps a `OneOfAssociatedValuesEncoder`.
-
- The encoder expects a single call to `nestedContainer(keyedBy:forKey:)`, all other access will fail.
- */
-final class OneOfKeyedEncoder: AbstractEncodingNode, KeyedEncodingContainerProtocol where Key: CodingKey {
-
- /// The encoded data consisting of the enum case, and the associated value.
- private var content: (key: IntKeyWrapper, value: OneOfAssociatedValuesContainer)?
-
- func encodeNil(forKey key: Key) throws {
- throw ProtobufEncodingError.invalidAccess("Unexpected call to `encodeNil(forKey:)` while encoding a `ProtobufOneOf` enum")
- }
-
- func encode(_ value: T, forKey key: Key) throws where T : Encodable {
- throw ProtobufEncodingError.invalidAccess("Unexpected call to `encode(_,forKey:)` while encoding a `ProtobufOneOf` enum")
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
- do {
- let wrapped = try wrapError(path: codingPath + [key]) { try IntKeyWrapper(key) }
- let container = OneOfAssociatedValuesEncoder(path: codingPath + [key], info: userInfo, optional: false)
- content = (wrapped, container)
- return KeyedEncodingContainer(container)
- } catch {
- let container = ProtoKeyedThrowingEncoder(error: error as! ProtobufEncodingError,
- path: codingPath + [key],
- info: userInfo)
- return KeyedEncodingContainer(container)
- }
- }
-
- func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
- let container = ProtoUnkeyedThrowingEncoder(
- error: .invalidAccess("Unexpected call to `encode(_,forKey:)` while encoding a `ProtobufOneOf` enum"),
- path: codingPath + [key], info: userInfo)
- return container
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(
- error: .invalidAccess("Unexpected call to `superEncoder()` while encoding a `ProtobufOneOf` enum"),
- path: codingPath, info: userInfo)
- }
-
- func superEncoder(forKey key: Key) -> Encoder {
- ProtoThrowingNode(
- error: .invalidAccess("Unexpected call to `superEncoder(forKey:)` while encoding a `ProtobufOneOf` enum"),
- path: codingPath, info: userInfo)
- }
-}
-
-extension OneOfKeyedEncoder: OneOfKeyedContainer {
-
- func getContent() throws -> (key: IntKeyWrapper, value: EncodingContainer) {
- guard let content = content else {
- throw ProtobufEncodingError.noContainersAccessed
- }
- let value = try content.value.getValue()
- return (content.key, value)
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictEncodingNode.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictEncodingNode.swift
deleted file mode 100644
index f409013..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictEncodingNode.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import Foundation
-
-final class ProtoDictEncodingNode: ProtoEncodingNode {
-
- override func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey {
- let container = wrap { ProtoDictKeyedEncoder(path: codingPath, info: userInfo, optional: false) }
- return KeyedEncodingContainer(container)
- }
-
- override func unkeyedContainer() -> UnkeyedEncodingContainer {
- wrap { ProtoDictUnkeyedEncoder(path: codingPath, info: userInfo, optional: false) }
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictKeyedEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictKeyedEncoder.swift
deleted file mode 100644
index e46d560..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictKeyedEncoder.swift
+++ /dev/null
@@ -1,92 +0,0 @@
-import Foundation
-
-final class ProtoDictKeyedEncoder: AbstractEncodingNode, KeyedEncodingContainerProtocol where Key: CodingKey {
-
- private var content = [EncodingContainer]()
-
- func assign(_ value: EncodingContainer, to key: EncodingContainer) {
- let pair = ProtoDictPair(key: key, value: value)
- content.append(pair)
- }
-
- func encodeNil(forKey key: Key) throws {
- throw ProtobufEncodingError.nilValuesNotSupported
- }
-
- func encode(_ value: T, forKey key: Key) throws where T : Encodable {
- let container: EncodingContainer
- if let primitive = value as? EncodablePrimitive {
- container = try wrapError(path: codingPath) { try EncodedPrimitive(protobuf: primitive) }
- } else {
- container = try ProtoEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- }
- let wrapped = try wrapError(path: codingPath) {
- try EncodedPrimitive(protobuf: key.intValue ?? key.stringValue)
- }
- assign(container, to: wrapped)
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
- do {
- let wrapped = try wrapError(path: codingPath) {
- try EncodedPrimitive(protobuf: key.intValue ?? key.stringValue)
- }
- let container = ProtoKeyedEncoder(path: codingPath + [key], info: userInfo, optional: false)
- assign(container, to: wrapped)
- return KeyedEncodingContainer(container)
- } catch {
- let container = ProtoKeyedThrowingEncoder(
- error: error as! ProtobufEncodingError,
- path: codingPath,
- info: userInfo)
- return KeyedEncodingContainer(container)
- }
- }
-
- func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
- do {
- let wrapped = try wrapError(path: codingPath) {
- try EncodedPrimitive(protobuf: key.intValue ?? key.stringValue)
- }
- let container = ProtoUnkeyedEncoder(path: codingPath + [key], info: userInfo, optional: false)
- assign(container, to: wrapped)
- return container
- } catch {
- let container = ProtoUnkeyedThrowingEncoder(
- error: error as! ProtobufEncodingError,
- path: codingPath,
- info: userInfo)
- return container
- }
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(error: .superNotSupported, path: codingPath, info: userInfo)
- }
-
- func superEncoder(forKey key: Key) -> Encoder {
- ProtoThrowingNode(error: .superNotSupported, path: codingPath, info: userInfo)
- }
-}
-
-
-extension ProtoDictKeyedEncoder: EncodingContainer {
-
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data {
- content
- .map { $0.encodeWithKey(key) }
- .joinedData
- }
-
- var data: Data {
- content.map { $0.dataWithLengthInformationIfRequired }.joinedData
- }
-
- var dataType: DataType {
- .variableLength
- }
-
- var isEmpty: Bool {
- !content.contains { !$0.isEmpty }
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictPair.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictPair.swift
deleted file mode 100644
index 841122e..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictPair.swift
+++ /dev/null
@@ -1,24 +0,0 @@
-import Foundation
-
-struct ProtoDictPair {
-
- let key: EncodingContainer
-
- let value: EncodingContainer
-}
-
-extension ProtoDictPair: EncodingContainer {
-
- var data: Data {
- key.encodeWithKey(try! IntKeyWrapper(value: 1))
- + value.encodeWithKey(try! IntKeyWrapper(value: 2))
- }
-
- var dataType: DataType {
- .variableLength
- }
-
- var isEmpty: Bool {
- false
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictUnkeyedEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictUnkeyedEncoder.swift
deleted file mode 100644
index fed0046..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoDictionary/ProtoDictUnkeyedEncoder.swift
+++ /dev/null
@@ -1,83 +0,0 @@
-import Foundation
-
-final class ProtoDictUnkeyedEncoder: AbstractEncodingNode, UnkeyedEncodingContainer {
-
- var count: Int {
- content.count
- }
-
- private var content = [ProtoDictPair]()
-
- private var key: EncodingContainer?
-
- private func assign(_ value: T) where T: EncodingContainer {
- if let key = key {
- let pair = ProtoDictPair(key: key, value: value)
- content.append(pair)
- self.key = nil
- } else {
- key = value
- }
- }
-
- func encodeNil() throws {
- throw ProtobufEncodingError.nilValuesNotSupported
- }
-
- func encode(_ value: T) throws where T : Encodable {
- if let primitive = value as? EncodablePrimitive {
- // Ensure that only same-type values are encoded
- if let first = content.first {
- if key == nil && first.key.dataType != primitive.dataType {
- throw ProtobufEncodingError.multipleTypesInUnkeyedContainer
- }
- if key != nil && first.value.dataType != primitive.dataType {
- throw ProtobufEncodingError.multipleTypesInUnkeyedContainer
- }
- }
-
- let node = try wrapError(path: codingPath) {
- try EncodedPrimitive(protobuf: primitive)
- }
- assign(node)
- return
- }
- let node = try ProtoEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- assign(node)
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey {
- // Should never happen, since this container type is only used for dictionaries
- fatalError()
- }
-
- func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
- // Should never happen, since this container type is only used for dictionaries
- fatalError()
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(error: .superNotSupported, path: codingPath, info: userInfo)
- }
-}
-
-extension ProtoDictUnkeyedEncoder: EncodingContainer {
-
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data {
- content
- .map { $0.encodeWithKey(key) }
- .joinedData
- }
-
- var data: Data {
- content.map { $0.dataWithLengthInformationIfRequired }.joinedData
- }
-
- var dataType: DataType {
- .variableLength
- }
-
- var isEmpty: Bool {
- content.isEmpty
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoEncodingNode.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoEncodingNode.swift
deleted file mode 100644
index 0f07f0a..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoEncodingNode.swift
+++ /dev/null
@@ -1,52 +0,0 @@
-import Foundation
-
-class ProtoEncodingNode: AbstractEncodingNode, Encoder {
-
- var container: EncodingContainer?
-
- func wrap(container: () -> T) -> T where T: EncodingContainer {
- guard self.container == nil else {
- fatalError("Multiple calls to `container<>(keyedBy:)`, `unkeyedContainer()`, or `singleValueContainer()` for an encoder")
- }
- let value = container()
- self.container = value
- return value
- }
-
- func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey {
- let container = wrap { ProtoKeyedEncoder(path: codingPath, info: userInfo, optional: false) }
- return KeyedEncodingContainer(container)
- }
-
- func unkeyedContainer() -> UnkeyedEncodingContainer {
- wrap { ProtoUnkeyedEncoder(path: codingPath, info: userInfo, optional: false) }
- }
-
- func singleValueContainer() -> SingleValueEncodingContainer {
- wrap { ProtoValueEncoder(path: codingPath, info: userInfo, optional: false) }
- }
-
- func encoding(_ value: T) throws -> Self where T: Encodable {
- try value.encode(to: self)
- return self
- }
-}
-
-extension ProtoEncodingNode: EncodingContainer {
-
- var data: Data {
- container!.data
- }
-
- var dataType: DataType {
- container!.dataType
- }
-
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data {
- container!.encodeWithKey(key)
- }
-
- var isEmpty: Bool {
- container?.isEmpty ?? true
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoKeyedThrowingEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoKeyedThrowingEncoder.swift
deleted file mode 100644
index e7d1948..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoKeyedThrowingEncoder.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-import Foundation
-
-final class ProtoKeyedThrowingEncoder: ProtoThrowingNode, KeyedEncodingContainerProtocol where Key: CodingKey {
-
- func encodeNil(forKey key: Key) throws {
- throw error
- }
-
- func encode(_ value: T, forKey key: Key) throws where T : Encodable {
- throw error
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = ProtoKeyedThrowingEncoder(from: self)
- return KeyedEncodingContainer(container)
- }
-
- func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
- ProtoUnkeyedThrowingEncoder(from: self)
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(from: self)
- }
-
- func superEncoder(forKey key: Key) -> Encoder {
- ProtoThrowingNode(from: self)
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoThrowingNode.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoThrowingNode.swift
deleted file mode 100644
index ff15af4..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoThrowingNode.swift
+++ /dev/null
@@ -1,49 +0,0 @@
-import Foundation
-
-/**
- A class used when features are not supported for protobuf encoding.
-
- Any calls to encoding functions will fail with a `ProtobufEncodingError` error
- */
-class ProtoThrowingNode: AbstractEncodingNode, Encoder {
-
- let error: ProtobufEncodingError
-
- init(error: ProtobufEncodingError, path: [CodingKey], info: UserInfo) {
- self.error = error
- super.init(path: path, info: info, optional: false)
- }
-
- init(from node: ProtoThrowingNode) {
- self.error = node.error
- super.init(path: node.codingPath, info: node.userInfo, optional: false)
- }
-
- func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey {
- let container = ProtoKeyedThrowingEncoder(from: self)
- return KeyedEncodingContainer(container)
- }
-
- func unkeyedContainer() -> UnkeyedEncodingContainer {
- ProtoUnkeyedThrowingEncoder(from: self)
- }
-
- func singleValueContainer() -> SingleValueEncodingContainer {
- ProtoValueThrowingEncoder(from: self)
- }
-}
-
-extension ProtoThrowingNode: EncodingContainer {
-
- var isNil: Bool { false }
-
- var data: Data {
- .empty
- }
-
- var dataType: DataType {
- .byte
- }
-
- var isEmpty: Bool { false }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoUnkeyedThrowingEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoUnkeyedThrowingEncoder.swift
deleted file mode 100644
index 2956160..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoUnkeyedThrowingEncoder.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-import Foundation
-
-final class ProtoUnkeyedThrowingEncoder: ProtoThrowingNode, UnkeyedEncodingContainer {
-
- var count: Int {
- 0
- }
-
- func encodeNil() throws {
- throw error
- }
-
- func encode(_ value: T) throws where T : Encodable {
- throw error
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = ProtoKeyedThrowingEncoder(from: self)
- return KeyedEncodingContainer(container)
- }
-
- func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
- ProtoUnkeyedThrowingEncoder(from: self)
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(from: self)
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoValueThrowingEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoValueThrowingEncoder.swift
deleted file mode 100644
index 8e5d65a..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoFail/ProtoValueThrowingEncoder.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-import Foundation
-
-final class ProtoValueThrowingEncoder: ProtoThrowingNode, SingleValueEncodingContainer {
-
- func encodeNil() throws {
- throw error
- }
-
- func encode(_ value: T) throws where T : Encodable {
- throw error
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoKeyedEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoKeyedEncoder.swift
deleted file mode 100644
index d4f8650..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoKeyedEncoder.swift
+++ /dev/null
@@ -1,101 +0,0 @@
-import Foundation
-
-final class ProtoKeyedEncoder: AbstractEncodingNode, KeyedEncodingContainerProtocol where Key: CodingKey {
-
- var content = [IntKeyWrapper : EncodingContainer]()
-
- func assign(_ value: EncodingContainer, to key: CodingKey) throws {
- let wrapped = try IntKeyWrapper(key)
- assign(value, to: wrapped)
- }
-
- func assign(_ value: EncodingContainer, to key: IntKeyWrapper) {
- content[key] = value
- }
-
- func encodeNil(forKey key: Key) throws {
- throw ProtobufEncodingError.nilValuesNotSupported
- }
-
- func encode(_ value: T, forKey key: Key) throws where T : Encodable {
- var wrappedKey = try IntKeyWrapper(key)
- let container: EncodingContainer
- if value is ProtobufOneOf {
- (wrappedKey, container) = try OneOfEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- } else if let primitive = value as? EncodablePrimitive {
- container = try wrapError(path: codingPath + [key]) {
- try EncodedPrimitive(protobuf: primitive, excludeDefaults: true)
- }
- } else if value is AnyDictionary {
- container = try ProtoDictEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- } else {
- container = try ProtoEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- }
- assign(container, to: wrappedKey)
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey {
- do {
- let wrapped = try IntKeyWrapper(key)
- let container = ProtoKeyedEncoder(path: codingPath + [key], info: userInfo, optional: false)
- assign(container, to: wrapped)
- return KeyedEncodingContainer(container)
- } catch {
- let container = ProtoKeyedThrowingEncoder(error: error as! ProtobufEncodingError,
- path: codingPath + [key],
- info: userInfo)
- return KeyedEncodingContainer(container)
- }
- }
-
- func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
- do {
- let wrapped = try IntKeyWrapper(key)
- let container = ProtoUnkeyedEncoder(path: codingPath + [key], info: userInfo, optional: false)
- assign(container, to: wrapped)
- return container
- } catch {
- let container = ProtoUnkeyedThrowingEncoder(
- error: error as! ProtobufEncodingError,
- path: codingPath,
- info: userInfo)
- return container
- }
- }
-
- func superEncoder() -> Encoder {
- ProtoThrowingNode(error: .superNotSupported, path: codingPath, info: userInfo)
- }
-
- func superEncoder(forKey key: Key) -> Encoder {
- ProtoThrowingNode(error: .superNotSupported, path: codingPath, info: userInfo)
- }
-}
-
-extension ProtoKeyedEncoder: EncodingContainer {
-
- private var nonEmptyValues: [(key: IntKeyWrapper, value: EncodingContainer)] {
- content.filter { !$0.value.isEmpty }
- }
-
- private var sortedKeysIfNeeded: [(key: IntKeyWrapper, value: EncodingContainer)] {
- guard sortKeysDuringEncoding else {
- return nonEmptyValues.map { $0 }
- }
- return nonEmptyValues.sorted { $0.key < $1.key }
- }
-
- var data: Data {
- sortedKeysIfNeeded.map { key, value -> Data in
- value.encodeWithKey(key)
- }.reduce(Data(), +)
- }
-
- var dataType: DataType {
- .variableLength
- }
-
- var isEmpty: Bool {
- !content.values.contains { !$0.isEmpty }
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoUnkeyedEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoUnkeyedEncoder.swift
deleted file mode 100644
index 9a8fc78..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoUnkeyedEncoder.swift
+++ /dev/null
@@ -1,98 +0,0 @@
-import Foundation
-
-final class ProtoUnkeyedEncoder: AbstractEncodingNode, UnkeyedEncodingContainer {
-
- var count: Int {
- content.count
- }
-
- private var content = [EncodingContainer]()
-
- @discardableResult
- private func assign(_ encoded: () throws -> T) rethrows -> T where T: EncodingContainer {
- let value = try encoded()
- content.append(value)
- return value
- }
-
- func encodeNil() throws {
- throw ProtobufEncodingError.nilValuesNotSupported
- }
-
- func encode(_ value: T) throws where T : Encodable {
- if let primitive = value as? EncodablePrimitive {
- // Ensure that only same-type values are encoded
-
- // TODO: Improve detection of same types
- // The Protobuf repeated fields must have the same type
- // Currently, we only check that the data type of the primitives matches,
- // so different types with the same DataType would not cause an error
- // This isn't a huge problem, since this could only happen if somebody would
- // write a custom encoding routine, so they would probably know that this breaks
- // Protobuf support.
- if let first = content.first, first.dataType != primitive.dataType {
- throw ProtobufEncodingError.multipleTypesInUnkeyedContainer
- }
-
- try assign {
- try wrapError(path: codingPath) {
- try EncodedPrimitive(protobuf: primitive)
- }
- }
- return
- }
- let node = try ProtoEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- assign { node }
- }
-
- func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = assign {
- ProtoKeyedEncoder(path: codingPath, info: userInfo, optional: false)
- }
- return KeyedEncodingContainer(container)
- }
-
- func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
- assign {
- ProtoUnkeyedEncoder(path: codingPath, info: userInfo, optional: false)
- }
- }
-
- func superEncoder() -> Encoder {
- assign {
- ProtoEncodingNode(path: codingPath, info: userInfo, optional: false)
- }
- }
-}
-
-extension ProtoUnkeyedEncoder: EncodingContainer {
-
- private var packedProtoData: Data {
- let data = self.data
- return data.count.variableLengthEncoding + data
- }
-
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data {
- // Don't prepend index set for protobuf, separate complex types
- if let first = content.first, first.dataType == .variableLength {
- // Unpacked
- return content
- .map { $0.encodeWithKey(key) }
- .joinedData
- }
- // Packed
- return key.encode(for: dataType) + packedProtoData
- }
-
- var data: Data {
- content.map { $0.dataWithLengthInformationIfRequired }.joinedData
- }
-
- var dataType: DataType {
- .variableLength
- }
-
- var isEmpty: Bool {
- !content.contains { !$0.isEmpty }
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/Protobuf/ProtoValueEncoder.swift b/Sources/BinaryCodable/Encoding/Protobuf/ProtoValueEncoder.swift
deleted file mode 100644
index e194815..0000000
--- a/Sources/BinaryCodable/Encoding/Protobuf/ProtoValueEncoder.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-import Foundation
-
-final class ProtoValueEncoder: AbstractEncodingNode, SingleValueEncodingContainer {
-
- private var container: EncodingContainer?
-
- func encodeNil() throws {
- try assign { nil }
- }
-
- private func assign(_ encoded: () throws -> EncodingContainer?) throws {
- guard container == nil else {
- throw ProtobufEncodingError.multipleValuesInSingleValueContainer
- }
- container = try encoded()
- }
-
- func encode(_ value: T) throws where T : Encodable {
- if let primitive = value as? EncodablePrimitive {
- try assign {
- try wrapError(path: codingPath) {
- try EncodedPrimitive(protobuf: primitive, excludeDefaults: true)
- }
- }
- return
- }
- try assign {
- try ProtoEncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- }
- }
-}
-
-extension ProtoValueEncoder: EncodingContainer {
-
- var data: Data {
- container?.data ?? .empty
- }
-
- var dataType: DataType {
- container!.dataType
- }
-
- var isEmpty: Bool {
- container?.isEmpty ?? true
- }
-}
diff --git a/Sources/BinaryCodable/Encoding/UnkeyedEncoder.swift b/Sources/BinaryCodable/Encoding/UnkeyedEncoder.swift
index bb8398a..8b4289c 100644
--- a/Sources/BinaryCodable/Encoding/UnkeyedEncoder.swift
+++ b/Sources/BinaryCodable/Encoding/UnkeyedEncoder.swift
@@ -1,91 +1,61 @@
import Foundation
final class UnkeyedEncoder: AbstractEncodingNode, UnkeyedEncodingContainer {
-
+
+ private var encodedValues: [EncodableContainer] = []
+
var count: Int {
- content.count + nilIndices.count
+ encodedValues.count
+ }
+
+ func encodeNil() throws {
+ encodedValues.append(NilContainer())
}
-
- private var content = [EncodingContainer]()
-
- private var nilIndices = Set()
-
+
@discardableResult
- private func assign(_ encoded: () throws -> T) rethrows -> T where T: EncodingContainer {
- let value = try encoded()
- content.append(value)
+ private func add(_ value: T) -> T where T: EncodableContainer {
+ encodedValues.append(value)
return value
}
-
- func encodeNil() throws {
- nilIndices.insert(count)
- }
-
- func encode(_ value: T) throws where T : Encodable {
- if value is AnyOptional {
- try assign {
- try EncodingNode(path: codingPath, info: userInfo, optional: true).encoding(value)
- }
- } else if let primitive = value as? EncodablePrimitive {
- try assign {
- try wrapError(path: codingPath) {
- try EncodedPrimitive(primitive: primitive)
- }
- }
- } else {
- let node = try EncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- assign { node }
- }
+
+ private func addedNode() -> EncodingNode {
+ let node = EncodingNode(needsLengthData: true, codingPath: codingPath, userInfo: userInfo)
+ return add(node)
}
-
+
func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey {
- let container = assign {
- KeyedEncoder(path: codingPath, info: userInfo, optional: false)
- }
- return KeyedEncodingContainer(container)
+ KeyedEncodingContainer(add(KeyedEncoder(needsLengthData: true, codingPath: codingPath, userInfo: userInfo)))
}
-
+
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
- assign {
- UnkeyedEncoder(path: codingPath, info: userInfo, optional: false)
- }
+ add(UnkeyedEncoder(needsLengthData: true, codingPath: codingPath, userInfo: userInfo))
}
-
+
func superEncoder() -> Encoder {
- assign {
- EncodingNode(path: codingPath, info: userInfo, optional: false)
- }
+ addedNode()
+ }
+
+ func encode(_ value: T) throws where T : Encodable {
+ let encoded = try encodeValue(value, needsLengthData: true)
+ add(encoded)
}
+
}
-extension UnkeyedEncoder: EncodingContainer {
+extension UnkeyedEncoder: EncodableContainer {
- private var rawIndicesData: Data {
- nilIndices.sorted().map { $0.variableLengthEncoding }.joinedData
- }
-
- private var nilIndicesData: Data {
- let count = nilIndices.count
- return count.variableLengthEncoding + rawIndicesData
- }
-
- private var contentData: Data {
- content.map { $0.dataWithLengthInformationIfRequired }.joinedData
+ var needsNilIndicator: Bool {
+ false
}
- var data: Data {
- if prependNilIndexSetForUnkeyedContainers {
- return nilIndicesData + contentData
- } else {
- return contentData
- }
- }
-
- var dataType: DataType {
- .variableLength
+ var isNil: Bool {
+ false
}
- var isEmpty: Bool {
- count == 0
+ func containedData() throws -> Data {
+ try encodedValues.map {
+ let data = try $0.completeData()
+ return data
+ }.joinedData
}
}
diff --git a/Sources/BinaryCodable/Encoding/ValueEncoder.swift b/Sources/BinaryCodable/Encoding/ValueEncoder.swift
index 94a6ab6..ba9f9a7 100644
--- a/Sources/BinaryCodable/Encoding/ValueEncoder.swift
+++ b/Sources/BinaryCodable/Encoding/ValueEncoder.swift
@@ -1,104 +1,41 @@
import Foundation
final class ValueEncoder: AbstractEncodingNode, SingleValueEncodingContainer {
-
- private var container: EncodingContainer?
- private var containerHasOptionalContent: Bool = false
- private var hasValue = false
-
- func encodeNil() throws {
- assign {
- containsOptional = true
- return nil
- }
+ private var encodedValue: EncodableContainer?
+
+ init(codingPath: [CodingKey], userInfo: [CodingUserInfoKey : Any]) {
+ super.init(needsLengthData: false, codingPath: codingPath, userInfo: userInfo)
}
-
- private func assign(_ encoded: () throws -> EncodingContainer?) rethrows {
- guard !hasValue else {
- fatalError("Attempt to encode multiple values in single value container")
+
+ func encodeNil() throws {
+ guard encodedValue == nil else {
+ throw EncodingError.invalidValue(0, .init(codingPath: codingPath, debugDescription: "Single value container: Multiple calls to encodeNil() or encode()"))
}
- container = try encoded()
- hasValue = true
+ encodedValue = NilContainer()
}
-
+
func encode(_ value: T) throws where T : Encodable {
- if value is AnyOptional {
- try assign {
- containerHasOptionalContent = true
- return try EncodingNode(path: codingPath, info: userInfo, optional: true).encoding(value)
- }
- } else if let primitive = value as? EncodablePrimitive {
- // Note: This assignment also work for optionals with a value, so
- // we need to check for optionals explicitly before
- try assign {
- try wrapError(path: codingPath) {
- try EncodedPrimitive(primitive: primitive)
- }
- }
- } else {
- try assign {
- try EncodingNode(path: codingPath, info: userInfo, optional: false).encoding(value)
- }
+ guard encodedValue == nil else {
+ throw EncodingError.invalidValue(value, .init(codingPath: codingPath, debugDescription: "Single value container: Multiple calls to encodeNil() or encode()"))
}
+ self.encodedValue = try encodeValue(value, needsLengthData: false)
}
}
-extension ValueEncoder: EncodingContainer {
-
- private var isNil: Bool { container == nil }
-
- var data: Data {
- if containsOptional {
- guard let container else {
- return Data([0])
- }
- return Data([1]) + container.dataWithLengthInformationIfRequired
- }
- return container?.data ?? .empty
- }
-
- var dataWithLengthInformationIfRequired: Data {
- if containsOptional {
- return data
- }
- guard dataType == .variableLength else {
- return data
- }
- return dataWithLengthInformation
- }
-
- var dataType: DataType {
- if containsOptional {
- return .variableLength
- }
- return container?.dataType ?? .byte
- }
+extension ValueEncoder: EncodableContainer {
- var isEmpty: Bool {
- container?.isEmpty ?? true
- }
+ var needsNilIndicator: Bool { true }
- private var optionalDataWithinKeyedContainer: Data {
- guard let container else {
- return Data([0])
- }
- guard !containerHasOptionalContent else {
- return container.data
- }
- return Data([1]) + container.dataWithLengthInformationIfRequired
+ var isNil: Bool {
+ encodedValue is NilContainer
}
- func encodeWithKey(_ key: CodingKeyWrapper) -> Data {
- if containsOptional || containerHasOptionalContent {
- guard !isNil else {
- // Nothing to do, nil is ommited for keyed containers
- return Data()
- }
- let data = optionalDataWithinKeyedContainer
- return key.encode(for: .variableLength) + data.count.variableLengthEncoding + data
- } else {
- return key.encode(for: container?.dataType ?? .byte) + (container?.dataWithLengthInformationIfRequired ?? .empty)
+ func containedData() throws -> Data {
+ guard let encodedValue else {
+ throw EncodingError.invalidValue(0, .init(codingPath: codingPath, debugDescription: "No value or nil encoded in single value container"))
}
+ let data = try encodedValue.completeData()
+ return data
}
}
diff --git a/Sources/BinaryCodable/ErrorHelper.swift b/Sources/BinaryCodable/ErrorHelper.swift
deleted file mode 100644
index 83e52d2..0000000
--- a/Sources/BinaryCodable/ErrorHelper.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-import Foundation
-
-func wrapError(path: [CodingKey], _ block: () throws -> T) rethrows -> T {
- do {
- return try block()
- } catch let error as EncodingError {
- switch error {
- case .invalidValue(let any, let context):
- let newContext = EncodingError.Context(
- codingPath: path + context.codingPath,
- debugDescription: context.debugDescription,
- underlyingError: context.underlyingError)
- throw EncodingError.invalidValue(any, newContext)
- @unknown default:
- throw error
- }
- } catch let error {
- let context = EncodingError.Context(codingPath: path, debugDescription: "An unknown error occured", underlyingError: error)
- throw EncodingError.invalidValue("", context)
- }
-}
diff --git a/Sources/BinaryCodable/Extensions/Data+Extensions.swift b/Sources/BinaryCodable/Extensions/Data+Extensions.swift
index ef32019..25c090b 100644
--- a/Sources/BinaryCodable/Extensions/Data+Extensions.swift
+++ b/Sources/BinaryCodable/Extensions/Data+Extensions.swift
@@ -25,15 +25,36 @@ extension Sequence where Element: Sequence, Element.Element == UInt8 {
}
}
-func read(data: Data, into value: T) -> T {
- Data(data).withUnsafeBytes {
- $0.baseAddress!.load(as: T.self)
+extension Data {
+
+ /**
+ Interpret the binary data as another type.
+ - Parameter type: The type to interpret
+ */
+ func interpreted(as type: T.Type = T.self) -> T {
+ Data(self).withUnsafeBytes {
+ $0.baseAddress!.load(as: T.self)
+ }
+ }
+
+ /**
+ Extract the binary representation of a value.
+ - Parameter value: The value to convert to binary data
+ */
+ init(underlying value: T) {
+ var target = value
+ self = Swift.withUnsafeBytes(of: &target) {
+ Data($0)
+ }
}
}
-func toData(_ value: T) -> Data {
- var target = value
- return withUnsafeBytes(of: &target) {
- Data($0)
+extension Optional {
+
+ var view: String {
+ guard let self else {
+ return "nil"
+ }
+ return "\(Array(self))"
}
}
diff --git a/Sources/BinaryCodable/Extensions/DecodingError+Extensions.swift b/Sources/BinaryCodable/Extensions/DecodingError+Extensions.swift
index ccb7983..6635350 100644
--- a/Sources/BinaryCodable/Extensions/DecodingError+Extensions.swift
+++ b/Sources/BinaryCodable/Extensions/DecodingError+Extensions.swift
@@ -2,25 +2,27 @@ import Foundation
extension DecodingError {
- static func variableLengthEncodedIntegerOutOfRange(_ path: [CodingKey]) -> DecodingError {
- corruptedError(path, description: "Encoded variable-length integer out of range")
+ static func variableLengthEncodedIntegerOutOfRange(_ codingPath: [CodingKey]) -> DecodingError {
+ corrupted("Encoded variable-length integer out of range", codingPath: codingPath)
}
- static func invalidDataSize(_ path: [CodingKey]) -> DecodingError {
- corruptedError(path, description: "Invalid data size")
+ static func notFound(_ key: CodingKey, codingPath: [CodingKey], _ message: String) -> DecodingError {
+ .keyNotFound(key, .init(codingPath: codingPath, debugDescription: message))
}
- static func multipleValuesForKey(_ path: [CodingKey], _ key: DecodingKey) -> DecodingError {
- corruptedError(path, description: "Multiple values for key \(key)")
+ static func valueNotFound(_ type: Any.Type, codingPath: [CodingKey], _ message: String) -> DecodingError {
+ .valueNotFound(type, .init(codingPath: codingPath, debugDescription: message))
}
- static func prematureEndOfData(_ path: [CodingKey]) -> DecodingError {
- corruptedError(path, description: "Premature end of data")
+ static func corrupted(_ message: String, codingPath: [CodingKey]) -> DecodingError {
+ return .dataCorrupted(.init(codingPath: codingPath, debugDescription: message))
}
- private static func corruptedError(_ path: [CodingKey], description: String) -> DecodingError {
- let context = DecodingError.Context(codingPath: path, debugDescription: description)
- return .dataCorrupted(context)
+ static func invalidSize(size: Int, for type: String, codingPath: [CodingKey]) -> DecodingError {
+ .dataCorrupted(.init(codingPath: codingPath, debugDescription: "Invalid size \(size) for type \(type)"))
}
+ static func prematureEndOfData(_ codingPath: [CodingKey]) -> DecodingError {
+ corrupted("Premature end of data", codingPath: codingPath)
+ }
}
diff --git a/Sources/BinaryCodable/Primitives/Bool+Coding.swift b/Sources/BinaryCodable/Primitives/Bool+Coding.swift
index 5d318e1..e17ba67 100644
--- a/Sources/BinaryCodable/Primitives/Bool+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Bool+Coding.swift
@@ -1,29 +1,27 @@
import Foundation
extension Bool: EncodablePrimitive {
-
- static var dataType: DataType {
- .variableLengthInteger
- }
-
- func data() -> Data {
+
+ var encodedData: Data {
Data([self ? 1 : 0])
}
}
extension Bool: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard data.count == 1 else {
- throw DecodingError.invalidDataSize(path)
+ throw DecodingError.invalidSize(size: data.count, for: "Bool", codingPath: codingPath)
+ }
+ let byte = data[data.startIndex]
+ switch byte {
+ case 0:
+ self = false
+ case 1:
+ self = true
+ default:
+ throw DecodingError.corrupted("Found value \(byte) while decoding boolean", codingPath: codingPath)
}
self = data[data.startIndex] > 0
}
}
-
-extension Bool: ProtobufCodable {
-
- var protoType: String { "bool" }
-
- static var zero: Bool { false }
-}
diff --git a/Sources/BinaryCodable/Primitives/Data+Coding.swift b/Sources/BinaryCodable/Primitives/Data+Coding.swift
index 3035a89..63e58c8 100644
--- a/Sources/BinaryCodable/Primitives/Data+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Data+Coding.swift
@@ -2,25 +2,14 @@ import Foundation
extension Data: EncodablePrimitive {
- static var dataType: DataType {
- .variableLength
- }
-
- func data() -> Data {
+ var encodedData: Data {
self
}
}
extension Data: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) {
- self = Data(data)
+ init(data: Data, codingPath: [CodingKey]) {
+ self = data
}
}
-
-extension Data: ProtobufCodable {
-
- var protoType: String { "bytes" }
-
- static var zero: Data { .empty }
-}
diff --git a/Sources/BinaryCodable/Primitives/Double+Coding.swift b/Sources/BinaryCodable/Primitives/Double+Coding.swift
index 02057f2..d99353e 100644
--- a/Sources/BinaryCodable/Primitives/Double+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Double+Coding.swift
@@ -1,39 +1,19 @@
import Foundation
extension Double: EncodablePrimitive {
-
- func data() -> Data {
- toData(bitPattern.bigEndian)
- }
-
- static var dataType: DataType {
- .eightBytes
+
+ var encodedData: Data {
+ .init(underlying: bitPattern.bigEndian)
}
}
extension Double: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
+ throw DecodingError.invalidSize(size: data.count, for: "Double", codingPath: codingPath)
}
- let value = UInt64(bigEndian: read(data: data, into: UInt64.zero))
+ let value = UInt64(bigEndian: data.interpreted())
self.init(bitPattern: value)
}
}
-
-extension Double: ProtobufEncodable {
-
- func protobufData() throws -> Data {
- data().swapped
- }
-
- var protoType: String { "double" }
-}
-
-extension Double: ProtobufDecodable {
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- try self.init(decodeFrom: data.swapped, path: path)
- }
-}
diff --git a/Sources/BinaryCodable/Primitives/Float+Coding.swift b/Sources/BinaryCodable/Primitives/Float+Coding.swift
index 5cc9cfc..1948d27 100644
--- a/Sources/BinaryCodable/Primitives/Float+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Float+Coding.swift
@@ -2,35 +2,19 @@ import Foundation
extension Float: EncodablePrimitive {
- func data() -> Data {
- toData(bitPattern.bigEndian)
- }
-
- static var dataType: DataType {
- .fourBytes
+ var encodedData: Data {
+ .init(underlying: bitPattern.bigEndian)
}
}
extension Float: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
+ throw DecodingError.invalidSize(size: data.count, for: "Float", codingPath: codingPath)
}
- let value = UInt32(bigEndian: read(data: data, into: UInt32.zero))
+ let value = UInt32(bigEndian: data.interpreted())
self.init(bitPattern: value)
}
}
-extension Float: ProtobufCodable {
-
- func protobufData() throws -> Data {
- data().swapped
- }
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- try self.init(decodeFrom: data.swapped, path: path)
- }
-
- var protoType: String { "float" }
-}
diff --git a/Sources/BinaryCodable/Primitives/Int+Coding.swift b/Sources/BinaryCodable/Primitives/Int+Coding.swift
index feaa770..ba26e80 100644
--- a/Sources/BinaryCodable/Primitives/Int+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Int+Coding.swift
@@ -1,104 +1,53 @@
import Foundation
extension Int: EncodablePrimitive {
-
- func data() -> Data {
- zigZagEncoded
- }
-
- static var dataType: DataType {
- .variableLengthInteger
- }
+
+ /// The integer encoded using zig-zag variable length encoding
+ var encodedData: Data { zigZagEncoded }
}
extension Int: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
- try self.init(fromZigZag: data, path: path)
+ init(data: Data, codingPath: [CodingKey]) throws {
+ try self.init(fromZigZag: data, codingPath: codingPath)
}
}
-extension Int: VariableLengthCodable {
-
- var variableLengthEncoding: Data {
- Int64(self).variableLengthEncoding
- }
-
- init(fromVarint data: Data, path: [CodingKey]) throws {
- let intValue = try Int64(fromVarint: data, path: path)
- guard let value = Int(exactly: intValue) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
- }
- self = value
- }
-}
-
-extension Int: FixedSizeCompatible {
+extension Int: ZigZagEncodable {
- public static var fixedSizeDataType: DataType {
- .eightBytes
+ var zigZagEncoded: Data {
+ Int64(self).zigZagEncoded
}
+}
- public var fixedProtoType: String {
- "sfixed64"
- }
+extension Int: ZigZagDecodable {
- public init(fromFixedSize data: Data, path: [CodingKey]) throws {
- let signed = try Int64(fromFixedSize: data, path: path)
- guard let value = Int(exactly: signed) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
+ init(fromZigZag data: Data, codingPath: [any CodingKey]) throws {
+ let raw = try Int64(data: data, codingPath: codingPath)
+ guard let value = Int(exactly: raw) else {
+ throw DecodingError.corrupted("Decoded value \(raw) is out of range for type Int", codingPath: codingPath)
}
self = value
}
-
- public var fixedSizeEncoded: Data {
- Int64(self).fixedSizeEncoded
- }
}
-extension Int: SignedValueCompatible {
- public var positiveProtoType: String {
- "int64"
+extension Int: VariableLengthEncodable {
+
+ /// The value encoded using variable length encoding
+ var variableLengthEncoding: Data {
+ Int64(self).variableLengthEncoding
}
}
-extension Int: ZigZagCodable {
-
- /**
- Encode a 64 bit signed integer using variable-length encoding.
-
- The sign of the value is extracted and appended as an additional bit.
- Positive signed values are thus encoded as `UInt(value) * 2`, and negative values as `UInt(abs(value) * 2 + 1`
+extension Int: VariableLengthDecodable {
- - Parameter value: The value to encode.
- - Returns: The value encoded as binary data (1 to 9 byte)
- */
- var zigZagEncoded: Data {
- Int64(self).zigZagEncoded
- }
-
- init(fromZigZag data: Data, path: [CodingKey]) throws {
- let raw = try Int64(fromZigZag: data, path: path)
- guard let value = Int(exactly: raw) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
+ init(fromVarint data: Data, codingPath: [CodingKey]) throws {
+ let intValue = try Int64(fromVarint: data, codingPath: codingPath)
+ guard let value = Int(exactly: intValue) else {
+ throw DecodingError.variableLengthEncodedIntegerOutOfRange(codingPath)
}
self = value
}
}
-extension Int: ProtobufEncodable {
-
- func protobufData() -> Data {
- variableLengthEncoding
- }
-
- var protoType: String { "sint64" }
-}
-
-extension Int: ProtobufDecodable {
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- try self.init(fromVarint: data, path: path)
- }
-}
diff --git a/Sources/BinaryCodable/Primitives/Int16+Coding.swift b/Sources/BinaryCodable/Primitives/Int16+Coding.swift
index 18a67de..70a107d 100644
--- a/Sources/BinaryCodable/Primitives/Int16+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Int16+Coding.swift
@@ -1,23 +1,19 @@
import Foundation
extension Int16: EncodablePrimitive {
-
- func data() -> Data {
- toData(UInt16(bitPattern: self).littleEndian)
- }
-
- static var dataType: DataType {
- .twoBytes
+
+ var encodedData: Data {
+ .init(underlying: UInt16(bitPattern: self).littleEndian)
}
}
extension Int16: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
+ throw DecodingError.invalidSize(size: data.count, for: "Int16", codingPath: codingPath)
}
- let value = UInt16(littleEndian: read(data: data, into: UInt16.zero))
+ let value = UInt16(littleEndian: data.interpreted())
self.init(bitPattern: value)
}
}
diff --git a/Sources/BinaryCodable/Primitives/Int32+Coding.swift b/Sources/BinaryCodable/Primitives/Int32+Coding.swift
index a5ade4d..e52a2d5 100644
--- a/Sources/BinaryCodable/Primitives/Int32+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Int32+Coding.swift
@@ -1,91 +1,48 @@
import Foundation
extension Int32: EncodablePrimitive {
-
- func data() -> Data {
- zigZagEncoded
- }
-
- static var dataType: DataType {
- .variableLengthInteger
- }
+
+ /// The integer encoded using zig-zag variable length encoding
+ var encodedData: Data { zigZagEncoded }
}
extension Int32: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
- try self.init(fromZigZag: data, path: path)
+ init(data: Data, codingPath: [CodingKey]) throws {
+ try self.init(fromZigZag: data, codingPath: codingPath)
}
}
-extension Int32: ZigZagCodable {
+extension Int32: ZigZagEncodable {
- /**
- Encode a 64 bit signed integer using variable-length encoding.
-
- The sign of the value is extracted and appended as an additional bit.
- Positive signed values are thus encoded as `UInt(value) * 2`, and negative values as `UInt(abs(value) * 2 + 1`
-
- - Parameter value: The value to encode.
- - Returns: The value encoded as binary data (1 to 9 byte)
- */
var zigZagEncoded: Data {
Int64(self).zigZagEncoded
}
-
- init(fromZigZag data: Data, path: [CodingKey]) throws {
- let raw = try Int64(fromZigZag: data, path: path)
- guard let value = Int32(exactly: raw) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
- }
- self = value
- }
}
-extension Int32: FixedSizeCompatible {
-
- public static var fixedSizeDataType: DataType {
- .fourBytes
- }
+extension Int32: ZigZagDecodable {
- public var fixedProtoType: String {
- "sfixed32"
- }
-
- public init(fromFixedSize data: Data, path: [CodingKey]) throws {
- guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
+ init(fromZigZag data: Data, codingPath: [any CodingKey]) throws {
+ let raw = try Int64(data: data, codingPath: codingPath)
+ guard let value = Int32(exactly: raw) else {
+ throw DecodingError.corrupted("Decoded value \(raw) is out of range for type Int32", codingPath: codingPath)
}
- let value = UInt32(littleEndian: read(data: data, into: UInt32.zero))
- self.init(bitPattern: value)
- }
-
- public var fixedSizeEncoded: Data {
- let value = UInt32(bitPattern: littleEndian)
- return toData(value)
+ self = value
}
}
-extension Int32: SignedValueCompatible {
+extension Int32: VariableLengthEncodable {
- public var positiveProtoType: String {
- "int32"
+ /// The value encoded using variable length encoding
+ var variableLengthEncoding: Data {
+ UInt32(bitPattern: self).variableLengthEncoding
}
}
-extension Int32: ProtobufCodable {
+extension Int32: VariableLengthDecodable {
- func protobufData() -> Data {
- Int64(self).protobufData()
+ init(fromVarint data: Data, codingPath: [CodingKey]) throws {
+ let value = try UInt32(data: data, codingPath: codingPath)
+ self = Int32(bitPattern: value)
}
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- let intValue = try Int64(fromProtobuf: data, path: path)
- guard let value = Int32(exactly: intValue) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
- }
- self = value
- }
-
- var protoType: String { "sint32" }
}
diff --git a/Sources/BinaryCodable/Primitives/Int64+Coding.swift b/Sources/BinaryCodable/Primitives/Int64+Coding.swift
index 269a9cb..02b2bca 100644
--- a/Sources/BinaryCodable/Primitives/Int64+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Int64+Coding.swift
@@ -1,46 +1,20 @@
import Foundation
extension Int64: EncodablePrimitive {
-
- func data() -> Data {
- zigZagEncoded
- }
-
- static var dataType: DataType {
- .variableLengthInteger
- }
+
+ /// The value encoded using zig-zag variable length encoding
+ var encodedData: Data { zigZagEncoded }
}
extension Int64: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
- try self.init(fromZigZag: data, path: path)
- }
-}
-
-extension Int64: VariableLengthCodable {
-
- var variableLengthEncoding: Data {
- UInt64(bitPattern: self).variableLengthEncoding
- }
-
- init(fromVarint data: Data, path: [CodingKey]) throws {
- let value = try UInt64(fromVarint: data, path: path)
- self = Int64(bitPattern: value)
+ init(data: Data, codingPath: [CodingKey]) throws {
+ try self.init(fromZigZag: data, codingPath: codingPath)
}
}
extension Int64: ZigZagEncodable {
-
- /**
- Encode a 64 bit signed integer using variable-length encoding.
-
- The sign of the value is extracted and appended as an additional bit.
- Positive signed values are thus encoded as `UInt(value) * 2`, and negative values as `UInt(abs(value) * 2 + 1`
-
- - Parameter value: The value to encode.
- - Returns: The value encoded as binary data (1 to 9 byte)
- */
+
var zigZagEncoded: Data {
guard self < 0 else {
return (UInt64(self.magnitude) << 1).variableLengthEncoding
@@ -50,10 +24,10 @@ extension Int64: ZigZagEncodable {
}
extension Int64: ZigZagDecodable {
-
- init(fromZigZag data: Data, path: [CodingKey]) throws {
- let unsigned = try UInt64(fromVarint: data, path: path)
-
+
+ init(fromZigZag data: Data, codingPath: [CodingKey]) throws {
+ let unsigned = try UInt64(data: data, codingPath: codingPath)
+
// Check the last bit to get sign
if unsigned & 1 > 0 {
// Divide by 2 and subtract one to get absolute value of negative values.
@@ -65,50 +39,19 @@ extension Int64: ZigZagDecodable {
}
}
-extension Int64: FixedSizeCompatible {
-
- static public var fixedSizeDataType: DataType {
- .eightBytes
- }
-
- public var fixedProtoType: String {
- "sfixed64"
- }
-
- public init(fromFixedSize data: Data, path: [CodingKey]) throws {
- guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
- }
- let value = UInt64(littleEndian: read(data: data, into: UInt64.zero))
- self.init(bitPattern: value)
- }
-
- public var fixedSizeEncoded: Data {
- let value = UInt64(bitPattern: littleEndian)
- return toData(value)
- }
-}
-
-extension Int64: SignedValueCompatible {
+extension Int64: VariableLengthEncodable {
- public var positiveProtoType: String {
- "int64"
- }
-}
-
-extension Int64: ProtobufEncodable {
-
- func protobufData() -> Data {
- variableLengthEncoding
+ /// The value encoded using variable length encoding
+ var variableLengthEncoding: Data {
+ UInt64(bitPattern: self).encodedData
}
- var protoType: String { "sint64" }
}
-extension Int64: ProtobufDecodable {
+extension Int64: VariableLengthDecodable {
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- try self.init(fromVarint: data, path: path)
+ init(fromVarint data: Data, codingPath: [CodingKey]) throws {
+ let value = try UInt64(data: data, codingPath: codingPath)
+ self = Int64(bitPattern: value)
}
-
}
diff --git a/Sources/BinaryCodable/Primitives/Int8+Coding.swift b/Sources/BinaryCodable/Primitives/Int8+Coding.swift
index f5f971e..e30c156 100644
--- a/Sources/BinaryCodable/Primitives/Int8+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/Int8+Coding.swift
@@ -1,21 +1,17 @@
import Foundation
extension Int8: EncodablePrimitive {
-
- func data() -> Data {
+
+ var encodedData: Data {
Data([UInt8(bitPattern: self)])
}
-
- static var dataType: DataType {
- .byte
- }
}
extension Int8: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard data.count == 1 else {
- throw DecodingError.invalidDataSize(path)
+ throw DecodingError.invalidSize(size: data.count, for: "Int8", codingPath: codingPath)
}
self.init(bitPattern: data[data.startIndex])
}
diff --git a/Sources/BinaryCodable/Primitives/String+Coding.swift b/Sources/BinaryCodable/Primitives/String+Coding.swift
index c15fa10..e7ed0fc 100644
--- a/Sources/BinaryCodable/Primitives/String+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/String+Coding.swift
@@ -1,33 +1,18 @@
import Foundation
extension String: EncodablePrimitive {
-
- static var dataType: DataType {
- .variableLength
- }
-
- func data() throws -> Data {
- guard let result = data(using: .utf8) else {
- throw EncodingError.invalidValue(self, .init(codingPath: [], debugDescription: "String is not UTF-8"))
- }
- return result
+
+ var encodedData: Data {
+ data(using: .utf8)!
}
}
extension String: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard let value = String(data: data, encoding: .utf8) else {
- let context = DecodingError.Context(codingPath: path, debugDescription: "Invalid string")
- throw DecodingError.dataCorrupted(context)
+ throw DecodingError.corrupted("Invalid string", codingPath: codingPath)
}
self = value
}
}
-
-extension String: ProtobufCodable {
-
- var protoType: String { "string" }
-
- static let zero = ""
-}
diff --git a/Sources/BinaryCodable/Primitives/UInt+Coding.swift b/Sources/BinaryCodable/Primitives/UInt+Coding.swift
index ada0a4a..fe06e6e 100644
--- a/Sources/BinaryCodable/Primitives/UInt+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/UInt+Coding.swift
@@ -1,74 +1,29 @@
import Foundation
extension UInt: EncodablePrimitive {
-
- func data() -> Data {
- variableLengthEncoding
- }
-
- static var dataType: DataType {
- .variableLengthInteger
- }
-}
-extension UInt: DecodablePrimitive {
-
- init(decodeFrom data: Data, path: [CodingKey]) throws {
- try self.init(fromVarint: data, path: path)
- }
+ var encodedData: Data { variableLengthEncoding }
}
-extension UInt: FixedSizeCompatible {
-
- static public var fixedSizeDataType: DataType {
- .eightBytes
- }
-
- public var fixedProtoType: String {
- "fixed64"
- }
+extension UInt: DecodablePrimitive {
- public init(fromFixedSize data: Data, path: [CodingKey]) throws {
- let intValue = try UInt64(fromFixedSize: data, path: path)
- guard let value = UInt(exactly: intValue) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
- }
- self = value
- }
+ init(data: Data, codingPath: [CodingKey]) throws {
+ try self.init(fromVarint: data, codingPath: codingPath)
- public var fixedSizeEncoded: Data {
- UInt64(self).fixedSizeEncoded
}
}
extension UInt: VariableLengthCodable {
-
+
var variableLengthEncoding: Data {
UInt64(self).variableLengthEncoding
}
-
- init(fromVarint data: Data, path: [CodingKey]) throws {
- let intValue = try UInt64(fromVarint: data, path: path)
- guard let value = UInt(exactly: intValue) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
+
+ init(fromVarint data: Data, codingPath: [any CodingKey]) throws {
+ let raw = try UInt64(fromVarint: data, codingPath: codingPath)
+ guard let value = UInt(exactly: raw) else {
+ throw DecodingError.corrupted("Decoded value \(raw) is out of range for type UInt", codingPath: codingPath)
}
self = value
}
}
-
-extension UInt: ProtobufEncodable {
-
- func protobufData() -> Data {
- variableLengthEncoding
- }
-
- var protoType: String { "uint64" }
-}
-
-extension UInt: ProtobufDecodable {
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- try self.init(fromVarint: data, path: path)
- }
-
-}
diff --git a/Sources/BinaryCodable/Primitives/UInt16+Coding.swift b/Sources/BinaryCodable/Primitives/UInt16+Coding.swift
index a0b7204..c087442 100644
--- a/Sources/BinaryCodable/Primitives/UInt16+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/UInt16+Coding.swift
@@ -1,22 +1,18 @@
import Foundation
extension UInt16: EncodablePrimitive {
-
- func data() -> Data {
- toData(littleEndian)
- }
-
- static var dataType: DataType {
- .twoBytes
+
+ var encodedData: Data {
+ .init(underlying: littleEndian)
}
}
extension UInt16: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
+ throw DecodingError.invalidSize(size: data.count, for: "UInt16", codingPath: codingPath)
}
- self.init(littleEndian: read(data: data, into: UInt16.zero))
+ self.init(littleEndian: data.interpreted())
}
}
diff --git a/Sources/BinaryCodable/Primitives/UInt32+Coding.swift b/Sources/BinaryCodable/Primitives/UInt32+Coding.swift
index 799875f..7f75187 100644
--- a/Sources/BinaryCodable/Primitives/UInt32+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/UInt32+Coding.swift
@@ -1,75 +1,33 @@
import Foundation
extension UInt32: EncodablePrimitive {
-
- func data() -> Data {
- variableLengthEncoding
- }
-
- static var dataType: DataType {
- .variableLengthInteger
- }
+
+ /// The value encoded using variable-length encoding
+ var encodedData: Data { variableLengthEncoding }
}
extension UInt32: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
- try self.init(fromVarint: data, path: path)
+ init(data: Data, codingPath: [CodingKey]) throws {
+ try self.init(fromVarint: data, codingPath: codingPath)
}
}
-extension UInt32: VariableLengthCodable {
-
+extension UInt32: VariableLengthEncodable {
+
+ /// The value encoded using variable-length encoding
var variableLengthEncoding: Data {
UInt64(self).variableLengthEncoding
}
-
- init(fromVarint data: Data, path: [CodingKey]) throws {
- let intValue = try UInt64(fromVarint: data, path: path)
- guard let value = UInt32(exactly: intValue) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
- }
- self = value
- }
-}
-
-extension UInt32: FixedSizeCompatible {
-
- static public var fixedSizeDataType: DataType {
- .fourBytes
- }
-
- public var fixedProtoType: String {
- "fixed32"
- }
-
- public init(fromFixedSize data: Data, path: [CodingKey]) throws {
- guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
- }
- self.init(littleEndian: read(data: data, into: UInt32.zero))
- }
-
- public var fixedSizeEncoded: Data {
- toData(littleEndian)
- }
-}
-
-extension UInt32: ProtobufEncodable {
-
- func protobufData() -> Data {
- UInt64(self).protobufData()
- }
- var protoType: String { "uint32" }
}
-extension UInt32: ProtobufDecodable {
+extension UInt32: VariableLengthDecodable {
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- let intValue = try UInt64.init(fromProtobuf: data, path: path)
- guard let value = UInt32(exactly: intValue) else {
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
+ init(fromVarint data: Data, codingPath: [any CodingKey]) throws {
+ let raw = try UInt64(fromVarint: data, codingPath: codingPath)
+ guard let value = UInt32(exactly: raw) else {
+ throw DecodingError.corrupted("Decoded value \(raw) is out of range for type UInt32", codingPath: codingPath)
}
self = value
}
diff --git a/Sources/BinaryCodable/Primitives/UInt64+Coding.swift b/Sources/BinaryCodable/Primitives/UInt64+Coding.swift
index b776d3a..71de901 100644
--- a/Sources/BinaryCodable/Primitives/UInt64+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/UInt64+Coding.swift
@@ -1,36 +1,21 @@
import Foundation
extension UInt64: EncodablePrimitive {
-
- func data() -> Data {
- variableLengthEncoding
- }
-
- static var dataType: DataType {
- .variableLengthInteger
- }
+
+ /// The value encoded using variable-length encoding
+ var encodedData: Data { variableLengthEncoding }
}
extension UInt64: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
- try self.init(fromVarint: data, path: path)
+ init(data: Data, codingPath: [CodingKey]) throws {
+ try self.init(fromVarint: data, codingPath: codingPath)
}
}
-extension UInt64: VariableLengthCodable {
-
- /**
- Encode a 64 bit unsigned integer using variable-length encoding.
-
- The first bit in each byte is used to indicate that another byte will follow it.
- So values from 0 to 2^7 - 1 (i.e. 127) will be encoded in a single byte.
- In general, `n` bytes are needed to encode values from ` 2^(n-1) ` to ` 2^n - 1`
- The maximum encodable value ` 2^64 - 1 ` is encoded as 9 byte.
-
- - Parameter value: The value to encode.
- - Returns: The value encoded as binary data (1 to 9 byte)
- */
+extension UInt64: VariableLengthEncodable {
+
+ /// The value encoded using variable-length encoding
var variableLengthEncoding: Data {
var result = Data()
var value = self
@@ -52,142 +37,16 @@ extension UInt64: VariableLengthCodable {
}
return result
}
-
- init(fromVarint data: Data, path: [CodingKey]) throws {
- var result: UInt64 = 0
-
- // There are always 7 usable bits per byte, for 8 bytes
- for byteIndex in 0..<8 {
- guard data.startIndex + byteIndex < data.endIndex else {
- throw DecodingError.prematureEndOfData(path)
- }
- let nextByte = UInt64(data[data.startIndex + byteIndex])
- // Insert the last 7 bit of the byte at the end
- result += UInt64(nextByte & 0x7F) << (byteIndex*7)
- // Check if an additional byte is coming
- guard nextByte & 0x80 > 0 else {
- self = result
- return
- }
- }
- guard data.startIndex + 8 < data.endIndex else {
- throw DecodingError.prematureEndOfData(path)
- }
- // The 9th byte has no next-byte bit, so all 8 bits are used
- let nextByte = UInt64(data[data.startIndex + 8])
- result += UInt64(nextByte) << 56
- self = result
- }
}
-extension UInt64: FixedSizeCompatible {
-
- static public var fixedSizeDataType: DataType {
- .eightBytes
- }
+extension UInt64: VariableLengthDecodable {
- public var fixedProtoType: String {
- "fixed64"
- }
-
- public init(fromFixedSize data: Data, path: [CodingKey]) throws {
- guard data.count == MemoryLayout.size else {
- throw DecodingError.invalidDataSize(path)
+ init(fromVarint data: Data, codingPath: [any CodingKey]) throws {
+ let storage = DecodingStorage(data: data, codingPath: codingPath)
+ let value = try storage.decodeUInt64()
+ guard storage.isAtEnd else {
+ throw DecodingError.corrupted("\(storage.numberOfRemainingBytes) unused bytes left after decoding variable length integer", codingPath: codingPath)
}
- self.init(littleEndian: read(data: data, into: UInt64.zero))
- }
-
- public var fixedSizeEncoded: Data {
- toData(littleEndian)
- }
-}
-
-extension UInt64: ZigZagEncodable {
-
- /**
- Encode a 64 bit unsigned integer using variable-length encoding.
-
- The first bit in each byte is used to indicate that another byte will follow it.
- So values from 0 to 2^7 - 1 (i.e. 127) will be encoded in a single byte.
- In general, `n` bytes are needed to encode values from ` 2^(n-1) ` to ` 2^n - 1 `
- The maximum encodable value ` 2^64 - 1 ` is encoded as 10 byte.
-
- - Parameter value: The value to encode.
- - Returns: The value encoded as binary data (1 to 10 byte)
- */
- var zigZagEncoded: Data {
- var result = Data()
- var value = self
- while true {
- // Extract 7 bit from value
- let nextByte = UInt8(value & 0x7F)
- value = value >> 7
- guard value > 0 else {
- result.append(nextByte)
- return result
- }
- // Set 8th bit to indicate another byte
- result.append(nextByte | 0x80)
- }
- }
-
-}
-
-extension UInt64: ZigZagDecodable {
-
- /**
- Extract a variable-length value from a container.
- - Parameter byteProvider: The data container with the encoded data.
- - Throws: `ProtobufDecodingError.invalidVarintEncoding` or
- `ProtobufDecodingError.missingData`
- - Returns: The decoded value.
- */
- init(fromZigZag data: Data, path: [CodingKey]) throws {
- var result: UInt64 = 0
-
- for byteIndex in 0...8 {
- guard data.startIndex + byteIndex < data.endIndex else {
- throw DecodingError.prematureEndOfData(path)
- }
- let nextByte = UInt64(data[data.startIndex + byteIndex])
- // Insert the last 7 bit of the byte at the end
- result += UInt64(nextByte & 0x7F) << (byteIndex*7)
- // Check if an additional byte is coming
- guard nextByte & 0x80 > 0 else {
- self = result
- return
- }
- }
- // If we're here, the 9th byte had the MSB set
- guard data.startIndex + 9 < data.endIndex else {
- throw DecodingError.prematureEndOfData(path)
- }
- let nextByte = data[data.startIndex + 9]
- switch nextByte {
- case 0:
- break
- case 1:
- result += 1 << 63
- default:
- // Only 0x01 and 0x00 are valid for the 10th byte, or the UInt64 would overflow
- throw DecodingError.variableLengthEncodedIntegerOutOfRange(path)
- }
- self = result
- }
-}
-
-extension UInt64: ProtobufEncodable {
-
- func protobufData() -> Data {
- variableLengthEncoding
- }
-
- var protoType: String { "uint64" }
-}
-
-extension UInt64: ProtobufDecodable {
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- try self.init(fromVarint: data, path: path)
+ self = value
}
}
diff --git a/Sources/BinaryCodable/Primitives/UInt8+Coding.swift b/Sources/BinaryCodable/Primitives/UInt8+Coding.swift
index 1469bf4..96a0fea 100644
--- a/Sources/BinaryCodable/Primitives/UInt8+Coding.swift
+++ b/Sources/BinaryCodable/Primitives/UInt8+Coding.swift
@@ -1,21 +1,17 @@
import Foundation
extension UInt8: EncodablePrimitive {
-
- func data() -> Data {
+
+ var encodedData: Data {
Data([self])
}
-
- static var dataType: DataType {
- .byte
- }
}
extension UInt8: DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws {
+ init(data: Data, codingPath: [CodingKey]) throws {
guard data.count == 1 else {
- throw DecodingError.invalidDataSize(path)
+ throw DecodingError.invalidSize(size: data.count, for: "UInt8", codingPath: codingPath)
}
self = data[data.startIndex]
}
diff --git a/Sources/BinaryCodable/ProtobufDecoder.swift b/Sources/BinaryCodable/ProtobufDecoder.swift
deleted file mode 100644
index 32bef69..0000000
--- a/Sources/BinaryCodable/ProtobufDecoder.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-import Foundation
-
-/**
- An encoder to convert protobuf binary data back to `Codable` objects.
-
- Decoding unsupported data types causes `DecodingError` or `ProtobufDecodingError` errors.
-
- To decode from data, instantiate a decoder and specify the type:
- ```
- let decoder = BinaryDecoder()
- let message = try decoder.decode(Message.self, from: data)
- ```
- Alternatively, the type can be inferred from context:
- ```
- func decode(data: Data) throws -> Message {
- try BinaryDecoder().decode(from: data)
- }
- ```
- There are also convenience functions to directly decode a single instance:
- ```
- let message = try BinaryDecoder.decode(Message.self, from: data)
- ```
- - Note: A single decoder can be used to decode multiple messages.
- */
-public final class ProtobufDecoder {
-
- /**
- Any contextual information set by the user for decoding.
-
- This dictionary is passed to all containers during the decoding process.
- */
- public var userInfo = [CodingUserInfoKey : Any]()
-
- /**
- Create a new binary encoder.
- - Note: A single decoder can be used to decode multiple messages.
- */
- public init() {
-
- }
-
- /**
- Decode a type from binary data.
- - Parameter type: The type to decode.
- - Parameter data: The binary data which encodes the instance
- - Returns: The decoded instance
- - Throws: Errors of type `DecodingError` or `ProtobufDecodingError`
- */
- public func decode(_ type: T.Type = T.self, from data: Data) throws -> T where T: Decodable {
- let root = ProtoDecodingNode(data: data, path: [], info: userInfo)
- return try type.init(from: root)
- }
-
- /**
- Decode a single value from binary data using a default decoder.
- - Parameter type: The type to decode.
- - Parameter data: The binary data which encodes the instance
- - Returns: The decoded instance
- - Throws: Errors of type `DecodingError` or `ProtobufDecodingError`
- */
- public static func decode(_ type: T.Type = T.self, from data: Data) throws -> T where T: Decodable {
- try ProtobufDecoder().decode(type, from: data)
- }
-}
diff --git a/Sources/BinaryCodable/ProtobufDecodingError.swift b/Sources/BinaryCodable/ProtobufDecodingError.swift
deleted file mode 100644
index 6284a99..0000000
--- a/Sources/BinaryCodable/ProtobufDecodingError.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-import Foundation
-
-/**
- An error produced while decoding binary data.
- */
-public enum ProtobufDecodingError: Error {
-
- case unexpectedDictionaryKey
-
- /**
- Protocol buffers don't support inheritance, so `super` can't be encoded.
- */
- case superNotSupported
-
- /**
- The encoded type contains a basic type that is not supported.
-
- The associated value contains a textual description of the unsupported type.
- */
- case unsupportedType(String)
-
- /**
- A decoding feature was accessed which is not supported for protobuf encoding.
-
- The associated value contains a textual description of the invalid access.
- */
- case invalidAccess(String)
-
-}
-
-extension ProtobufDecodingError {
-
- static func unsupported(type t: T.Type) -> ProtobufDecodingError {
- .unsupportedType("\(type(of: t))")
- }
-}
diff --git a/Sources/BinaryCodable/ProtobufEncoder.swift b/Sources/BinaryCodable/ProtobufEncoder.swift
deleted file mode 100644
index 9b47681..0000000
--- a/Sources/BinaryCodable/ProtobufEncoder.swift
+++ /dev/null
@@ -1,68 +0,0 @@
-import Foundation
-
-/**
- An encoder to convert `Codable` objects to protobuf binary data.
-
- The encoder provides only limited compatibility with Google's Protocol Buffers.
-
- Encoding unsupported data types causes `ProtobufEncodingError` errors.
-
- Construct an encoder when converting instances to binary data, and feed the message(s) into it:
-
- ```swift
- let message: Message = ...
-
- let encoder = ProtobufEncoder()
- let data = try encoder.encode(message)
- ```
-
- - Note: An ecoder can be used to encode multiple messages.
- */
-public final class ProtobufEncoder {
-
- /**
- Any contextual information set by the user for encoding.
-
- This dictionary is passed to all containers during the encoding process.
-
- Contains also keys for any custom options set for the encoder.
- See `sortKeysDuringEncoding`.
- */
- public var userInfo = [CodingUserInfoKey : Any]()
-
- /**
- Create a new binary encoder.
- - Note: An encoder can be used to encode multiple messages.
- */
- public init() {
-
- }
-
- /**
- Encode a value to binary data.
- - Parameter value: The value to encode
- - Returns: The encoded data
- - Throws: Errors of type `EncodingError` or `ProtobufEncodingError`
- */
- public func encode(_ value: T) throws -> Data where T: Encodable {
- let root = ProtoEncodingNode(path: [], info: userInfo, optional: false)
- try value.encode(to: root)
- return root.data
- }
-
- /**
- Encode a single value to binary data using a default encoder.
- - Parameter value: The value to encode
- - Returns: The encoded data
- - Throws: Errors of type `EncodingError` or `ProtobufEncodingError`
- */
- public static func encode(_ value: T) throws -> Data where T: Encodable {
- try ProtobufEncoder().encode(value)
- }
-
- func getProtobufDefinition(_ value: T) throws -> String where T: Encodable {
- let root = try ProtoNode(encoding: "\(type(of: value))", path: [], info: userInfo)
- .encoding(value)
- return try "syntax = \"proto3\";\n\n" + root.protobufDefinition()
- }
-}
diff --git a/Sources/BinaryCodable/ProtobufEncodingError.swift b/Sources/BinaryCodable/ProtobufEncodingError.swift
deleted file mode 100644
index f3a422e..0000000
--- a/Sources/BinaryCodable/ProtobufEncodingError.swift
+++ /dev/null
@@ -1,88 +0,0 @@
-import Foundation
-
-/**
- An error thrown when encoding a value using `ProtobufEncoder`.
- */
-public enum ProtobufEncodingError: Error {
-
- /**
- A procedural error occuring during encoding.
-
- A custom implementation of `func encode(to encoder: Encoder) throws` tried to encode multiple values into a single value container:
-
- ```
- func encode(to encoder: Encoder) throws {
- var container = encoder.singleValueContainer()
- container.encode(...)
- container.encode(...) // Invalid
- }
- ```
- */
- case multipleValuesInSingleValueContainer
-
- case noValueInSingleValueContainer
-
- /**
- The encoded type contains optional values, which are not supported in the protocol buffer format.
- */
- case nilValuesNotSupported
-
- /**
- The encoded type contains a basic type that is not supported.
-
- The associated value contains a textual description of the unsupported type.
- */
- case unsupportedType(String)
-
- /**
- Protocol buffers don't support inheritance, so `super` can't be encoded.
- */
- case superNotSupported
-
- /**
- The encoded type contains properties which don't have an integer key.
-
- The associated value contains the string key which is missing an integer key.
- */
- case missingIntegerKey(String)
-
- /**
- All values in unkeyed containers must have the same type.
- */
- case multipleTypesInUnkeyedContainer
-
- /**
- Field numbers must be positive integers not greater than `536870911` (`2^29-1`, or `0x1FFFFFFF`)
-
- The associated value is the integer key that is out of range.
- */
- case integerKeyOutOfRange(Int)
-
- /**
- Multiple calls to `container<>(keyedBy:)`, `unkeyedContainer()`, or `singleValueContainer()` for an encoder.
- */
- case multipleContainersAccessed
-
- /**
- No calls to `container<>(keyedBy:)`, `unkeyedContainer()`, or `singleValueContainer()` for an encoder.
- */
- case noContainersAccessed
-
- /**
- Protobuf requires an unkeyed container as the root node
- */
- case rootIsNotKeyedContainer
-
- /**
- An unavailable encoding feature was accessed.
-
- The associated value contains a textual description of the unsupported access.
- */
- case invalidAccess(String)
-
- case protobufDefinitionUnavailable(String)
-
- static func unsupported(type value: EncodablePrimitive) -> ProtobufEncodingError {
- .unsupportedType("\(type(of: value))")
- }
-}
diff --git a/Sources/BinaryCodable/Protocols/AnyDictionary.swift b/Sources/BinaryCodable/Protocols/AnyDictionary.swift
deleted file mode 100644
index ee9b819..0000000
--- a/Sources/BinaryCodable/Protocols/AnyDictionary.swift
+++ /dev/null
@@ -1,14 +0,0 @@
-import Foundation
-
-/**
- An abstract Dictionary, without any `self` requirements.
-
- This protocol is only adopted by `Dictionary`, and used to correctly encode and decode dictionaries for Protobuf-compatible encoding.
- */
-protocol AnyDictionary {
-
-}
-
-extension Dictionary: AnyDictionary {
-
-}
diff --git a/Sources/BinaryCodable/Protocols/AnyOptional.swift b/Sources/BinaryCodable/Protocols/AnyOptional.swift
deleted file mode 100644
index e4bf804..0000000
--- a/Sources/BinaryCodable/Protocols/AnyOptional.swift
+++ /dev/null
@@ -1,25 +0,0 @@
-import Foundation
-
-protocol AnyOptional {
-
- var isNil: Bool { get }
-
- static var nilValue: Self { get }
-}
-
-extension Optional: AnyOptional {
-
- var isNil: Bool {
- switch self {
- case .none:
- return true
- default:
- return false
- }
- }
-
- static var nilValue: Self {
- return Self.none
- }
-
-}
diff --git a/Sources/BinaryCodable/Protocols/DataTypeProvider.swift b/Sources/BinaryCodable/Protocols/DataTypeProvider.swift
deleted file mode 100644
index 3311940..0000000
--- a/Sources/BinaryCodable/Protocols/DataTypeProvider.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import Foundation
-
-protocol DataTypeProvider {
-
- static var dataType: DataType { get }
-}
-
-extension DataTypeProvider {
-
- var dataType: DataType {
- Self.dataType
- }
-}
diff --git a/Sources/BinaryCodable/Protocols/DecodablePrimitive.swift b/Sources/BinaryCodable/Protocols/DecodablePrimitive.swift
index ab508be..b6c7464 100644
--- a/Sources/BinaryCodable/Protocols/DecodablePrimitive.swift
+++ b/Sources/BinaryCodable/Protocols/DecodablePrimitive.swift
@@ -1,6 +1,13 @@
import Foundation
-protocol DecodablePrimitive: DataTypeProvider {
+/**
+ A protocol adopted by primitive types for decoding.
+ */
+protocol DecodablePrimitive {
- init(decodeFrom data: Data, path: [CodingKey]) throws
+ /**
+ Decode a value from the data.
+ - Note: All provided data can be used
+ */
+ init(data: Data, codingPath: [CodingKey]) throws
}
diff --git a/Sources/BinaryCodable/Protocols/EncodablePrimitive.swift b/Sources/BinaryCodable/Protocols/EncodablePrimitive.swift
index e9e2f0b..0ae32f4 100644
--- a/Sources/BinaryCodable/Protocols/EncodablePrimitive.swift
+++ b/Sources/BinaryCodable/Protocols/EncodablePrimitive.swift
@@ -1,6 +1,13 @@
import Foundation
-protocol EncodablePrimitive: DataTypeProvider {
-
- func data() throws -> Data
+/**
+ A protocol adopted by all base types (Int, Data, String, ...) to provide the encoded data.
+ */
+protocol EncodablePrimitive {
+
+ /**
+ The raw data of the encoded base type value.
+ - Note: No length information must be included
+ */
+ var encodedData: Data { get }
}
diff --git a/Sources/BinaryCodable/Protocols/ProtobufCodable.swift b/Sources/BinaryCodable/Protocols/ProtobufCodable.swift
deleted file mode 100644
index 5f1a659..0000000
--- a/Sources/BinaryCodable/Protocols/ProtobufCodable.swift
+++ /dev/null
@@ -1,40 +0,0 @@
-import Foundation
-
-typealias ProtobufCodable = ProtobufEncodable & ProtobufDecodable
-
-protocol ProtobufEncodable {
-
- func protobufData() throws -> Data
-
- var protoType: String { get }
-
- var isZero: Bool { get }
-}
-
-protocol ProtobufDecodable {
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws
-
- static var zero: Self { get }
-
-}
-
-extension ProtobufEncodable where Self: Equatable, Self: ProtobufDecodable {
-
- var isZero: Bool { self == .zero }
-}
-
-
-extension ProtobufEncodable where Self: EncodablePrimitive {
-
- func protobufData() throws -> Data {
- try data()
- }
-}
-
-extension ProtobufDecodable where Self: DecodablePrimitive {
-
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- try self.init(decodeFrom: data, path: path)
- }
-}
diff --git a/Sources/BinaryCodable/Protocols/ProtobufOneOf.swift b/Sources/BinaryCodable/Protocols/ProtobufOneOf.swift
deleted file mode 100644
index bdf54f9..0000000
--- a/Sources/BinaryCodable/Protocols/ProtobufOneOf.swift
+++ /dev/null
@@ -1,36 +0,0 @@
-import Foundation
-
-/**
- Add conformance to this protocol to enums which should be encoded as Protobuf `Oneof` values.
-
- Conform a suitable enum to the `ProtobufOneOf` protocol to indicate that it should be encoded as single value.
-
- The enum must have exactly one associated value for each case, and the integer keys must not overlap with the ones defined for the enclosing struct.
- ```swift
- struct Test: Codable {
-
- // The oneof field
- let alternatives: OneOf
-
- // The OneOf definition
- enum OneOf: Codable, ProtobufOneOf {
- case integer(Int)
- case string(String)
-
- // Field values, must not overlap with `Test.CodingKeys`
- enum CodingKeys: Int, CodingKey {
- case integer = 1
- case string = 2
- }
- }
-
- enum CodingKeys: Int, CodingKey {
- // The field id of the Oneof field is not used
- case alternatives = 123456
- }
- }
- ```
-
- See: [Protocol Buffer Language Guide: Oneof](https://developers.google.com/protocol-buffers/docs/proto3#oneof) and [Swift Protobuf: Oneof Fields](https://github.com/apple/swift-protobuf/blob/main/Documentation/API.md#oneof-fields)
- */
-public protocol ProtobufOneOf { }
diff --git a/Sources/BinaryCodable/Protocols/VariableLengthCodable.swift b/Sources/BinaryCodable/Protocols/VariableLengthCodable.swift
index d02093c..59eb867 100644
--- a/Sources/BinaryCodable/Protocols/VariableLengthCodable.swift
+++ b/Sources/BinaryCodable/Protocols/VariableLengthCodable.swift
@@ -1,8 +1,13 @@
import Foundation
-protocol VariableLengthCodable {
-
+typealias VariableLengthCodable = VariableLengthEncodable & VariableLengthDecodable
+
+protocol VariableLengthEncodable: FixedWidthInteger {
+
var variableLengthEncoding: Data { get }
-
- init(fromVarint data: Data, path: [CodingKey]) throws
+}
+
+protocol VariableLengthDecodable: FixedWidthInteger {
+
+ init(fromVarint data: Data, codingPath: [CodingKey]) throws
}
diff --git a/Sources/BinaryCodable/Protocols/ZigZagCodable.swift b/Sources/BinaryCodable/Protocols/ZigZagCodable.swift
index a03d337..bef76e1 100644
--- a/Sources/BinaryCodable/Protocols/ZigZagCodable.swift
+++ b/Sources/BinaryCodable/Protocols/ZigZagCodable.swift
@@ -10,6 +10,6 @@ protocol ZigZagEncodable {
protocol ZigZagDecodable {
- init(fromZigZag data: Data, path: [CodingKey]) throws
+ init(fromZigZag data: Data, codingPath: [CodingKey]) throws
}
diff --git a/Sources/BinaryCodable/Wrappers/FixedSize.swift b/Sources/BinaryCodable/Wrappers/FixedSize.swift
index 6b2fc39..6ae96d3 100644
--- a/Sources/BinaryCodable/Wrappers/FixedSize.swift
+++ b/Sources/BinaryCodable/Wrappers/FixedSize.swift
@@ -13,17 +13,13 @@ import Foundation
}
```
-The `FixedSize` property wrapper is supported for `UInt32`, `UInt64`, `Int32`, and `Int64` types.
+The `FixedSize` property wrapper is supported for `UInt`, `UInt32`, `UInt64`, `Int`, `Int32`, and `Int64` types.
+ - Warning: Do not conform other types to `FixedSizeCodable`. This will lead to crashes during encoding and decoding.
- SeeAlso: [Laguage Guide (proto3): Scalar value types](https://developers.google.com/protocol-buffers/docs/proto3#scalar)
*/
@propertyWrapper
-public struct FixedSize: ExpressibleByIntegerLiteral
-where WrappedValue: FixedSizeCompatible,
- WrappedValue: FixedWidthInteger,
- WrappedValue: Codable {
-
- public typealias IntegerLiteralType = WrappedValue.IntegerLiteralType
+public struct FixedSize where WrappedValue: FixedSizeCodable, WrappedValue: FixedWidthInteger {
/// The value wrapped in the fixed-size container
public var wrappedValue: WrappedValue
@@ -35,38 +31,185 @@ where WrappedValue: FixedSizeCompatible,
public init(wrappedValue: WrappedValue) {
self.wrappedValue = wrappedValue
}
+}
- public init(integerLiteral value: WrappedValue.IntegerLiteralType) {
- self.wrappedValue = WrappedValue.init(integerLiteral: value)
+extension FixedSize: 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: FixedSize, rhs: FixedSize) -> FixedSize {
+ .init(wrappedValue: lhs.wrappedValue * rhs.wrappedValue)
}
+ public static func *= (lhs: inout FixedSize, rhs: FixedSize) {
+ lhs.wrappedValue *= rhs.wrappedValue
+ }
}
-extension FixedSize: ProtobufEncodable where WrappedValue: ProtobufDecodable {
+extension FixedSize: AdditiveArithmetic {
- func protobufData() throws -> Data {
- wrappedValue.fixedSizeEncoded
+ /**
+ The zero value.
+
+ Zero is the identity element for addition. For any value, x + .zero == x and .zero + x == x.
+ */
+ public static var zero: Self {
+ .init(wrappedValue: .zero)
+ }
+
+ public static func - (lhs: FixedSize, rhs: FixedSize) -> FixedSize {
+ .init(wrappedValue: lhs.wrappedValue - rhs.wrappedValue)
}
- var protoType: String {
- wrappedValue.fixedProtoType
+ public static func + (lhs: FixedSize, rhs: FixedSize) -> FixedSize {
+ .init(wrappedValue: lhs.wrappedValue + rhs.wrappedValue)
}
}
-extension FixedSize: ProtobufDecodable where WrappedValue: ProtobufDecodable {
+extension FixedSize: BinaryInteger {
- static var zero: FixedSize {
- .init(wrappedValue: .zero)
+ 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: FixedSize, rhs: FixedSize) -> FixedSize {
+ .init(wrappedValue: lhs.wrappedValue / rhs.wrappedValue)
+ }
+
+ public static func % (lhs: FixedSize, rhs: FixedSize) -> FixedSize {
+ .init(wrappedValue: lhs.wrappedValue % rhs.wrappedValue)
+ }
+
+ public static func /= (lhs: inout FixedSize, rhs: FixedSize) {
+ lhs.wrappedValue /= rhs.wrappedValue
}
- init(fromProtobuf data: Data, path: [CodingKey]) throws {
- let value = try WrappedValue(fromFixedSize: data, path: path)
- self.init(wrappedValue: value)
+ public static func %= (lhs: inout FixedSize, rhs: FixedSize) {
+ lhs.wrappedValue %= rhs.wrappedValue
+ }
+
+ public static func &= (lhs: inout FixedSize, rhs: FixedSize) {
+ lhs.wrappedValue &= rhs.wrappedValue
+ }
+
+ public static func |= (lhs: inout FixedSize, rhs: FixedSize) {
+ lhs.wrappedValue |= rhs.wrappedValue
+ }
+
+ public static func ^= (lhs: inout FixedSize, rhs: FixedSize) {
+ lhs.wrappedValue ^= rhs.wrappedValue
+ }
+}
+
+extension FixedSize: FixedWidthInteger {
+
+ public typealias Words = WrappedValue.Words
+
+ public typealias Magnitude = WrappedValue.Magnitude
+
+ public init(_truncatingBits source: T) where T : BinaryInteger {
+ self.init(wrappedValue: .init(source))
+ }
+
+ public func dividingFullWidth(_ dividend: (high: FixedSize, low: WrappedValue.Magnitude)) -> (quotient: FixedSize, remainder: FixedSize) {
+ let result = wrappedValue.dividingFullWidth((high: dividend.high.wrappedValue, low: dividend.low))
+ return (quotient: FixedSize(wrappedValue: result.quotient), remainder: FixedSize(wrappedValue: result.remainder))
+ }
+
+ public func addingReportingOverflow(_ rhs: FixedSize) -> (partialValue: FixedSize, overflow: Bool) {
+ let result = wrappedValue.addingReportingOverflow(rhs.wrappedValue)
+ return (FixedSize(wrappedValue: result.partialValue), result.overflow)
+ }
+
+ public func subtractingReportingOverflow(_ rhs: FixedSize) -> (partialValue: FixedSize, overflow: Bool) {
+ let result = wrappedValue.subtractingReportingOverflow(rhs.wrappedValue)
+ return (FixedSize(wrappedValue: result.partialValue), result.overflow)
+ }
+
+ public func multipliedReportingOverflow(by rhs: FixedSize) -> (partialValue: FixedSize, overflow: Bool) {
+ let result = wrappedValue.multipliedReportingOverflow(by: rhs.wrappedValue)
+ return (FixedSize(wrappedValue: result.partialValue), result.overflow)
+ }
+
+ public func dividedReportingOverflow(by rhs: FixedSize) -> (partialValue: FixedSize, overflow: Bool) {
+ let result = wrappedValue.dividedReportingOverflow(by: rhs.wrappedValue)
+ return (FixedSize(wrappedValue: result.partialValue), result.overflow)
+ }
+
+ public func remainderReportingOverflow(dividingBy rhs: FixedSize