diff --git a/.gitignore b/.gitignore index 02c0875..09a1e7c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.build /Packages /*.xcodeproj +bin/protoc diff --git a/.travis.yml b/.travis.yml index 8fb0a32..7248413 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,9 @@ matrix: os: osx osx_image: xcode10.1 language: swift + before_script: + - wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-osx-x86_64.zip + - unzip protoc-3.6.1-osx-x86_64.zip script: - swift test @@ -20,6 +23,8 @@ matrix: dist: xenial language: c before_script: + - wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip + - unzip protoc-3.6.1-linux-x86_64.zip - wget https://swift.org/builds/swift-4.2.2-release/ubuntu1604/swift-4.2.2-RELEASE/swift-4.2.2-RELEASE-ubuntu16.04.tar.gz - tar xzf swift-4.2.2-RELEASE-ubuntu16.04.tar.gz - export PATH=$(pwd)/swift-4.2.2-RELEASE-ubuntu16.04/usr/bin:"${PATH}" @@ -32,9 +37,11 @@ matrix: dist: xenial language: c before_script: - - wget "https://swift.org/builds/development/ubuntu1604/swift-DEVELOPMENT-SNAPSHOT-2019-02-14-a/swift-DEVELOPMENT-SNAPSHOT-2019-02-14-a-ubuntu16.04.tar.gz" - - tar xzf swift-DEVELOPMENT-SNAPSHOT-2019-02-14-a-ubuntu16.04.tar.gz - - export PATH=$(pwd)/swift-DEVELOPMENT-SNAPSHOT-2019-02-14-a-ubuntu16.04/usr/bin:"${PATH}" + - wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-linux-x86_64.zip + - unzip protoc-3.6.1-linux-x86_64.zip + - wget "https://swift.org/builds/development/ubuntu1604/swift-DEVELOPMENT-SNAPSHOT-2019-02-19-a/swift-DEVELOPMENT-SNAPSHOT-2019-02-19-a-ubuntu16.04.tar.gz" + - tar xzf swift-DEVELOPMENT-SNAPSHOT-2019-02-19-a-ubuntu16.04.tar.gz + - export PATH=$(pwd)/swift-DEVELOPMENT-SNAPSHOT-2019-02-19-a-ubuntu16.04/usr/bin:"${PATH}" - swift --version script: - swift test diff --git a/BinaryCodable.podspec b/BinaryCodable.podspec index 81b36eb..0da54bd 100644 --- a/BinaryCodable.podspec +++ b/BinaryCodable.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BinaryCodable' - s.version = '0.1.0' + s.version = '0.2.0' s.license = 'Apache 2.0' s.summary = 'Codable-like interfaces for binary representations.' s.homepage = 'https://github.com/jverkoey/BinaryCodable' diff --git a/CHANGELOG.md b/CHANGELOG.md index 766626f..ddc19fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,66 @@ +# 0.2.0 + +This minor unstable release renames `sequentialContainer` to `container` for both the BinaryEncoder and BinaryDecoder types, adds support for `BinaryFloatingPoint` decoding and encoding, automatic BinaryCodable conformance for floating point and array types, and a variety of performance improvements. + +## Breaking changes + +`BinaryDecoder` and `BinaryEncoder`'s `sequentialContainer` has been renamed to `container`. This is a straightforward find-and-replace operation. + +## New features + +`Array` types automatically conform to BinaryCodable. It's easier now to code sequential binary objects like so: + +```swift +// Decoding +let objects = try container.decode([SomeBinaryObject].self) + +// Encoding +try container.encode(objects) +``` + +`Float` and `Double` RawRepresentable types automatically conform to BinaryCodable. + +## Source changes + +* [Remove excessive conditionals and buffer containment. (#40)](https://github.com/jverkoey/BinaryCodable/commit/4daaee9f3cf4c66da0f488940f5681e03b510306) (featherless) +* [Remove unnecessary cast.](https://github.com/jverkoey/BinaryCodable/commit/5308cd3139368daac29f5f60b4fc22ae6cc59a57) (Jeff Verkoeyen) +* [Fix bug and improve performance. (#39)](https://github.com/jverkoey/BinaryCodable/commit/1e9c21bc4be6e5501825665c0a73f9e87b909ffa) (featherless) +* [Remove unnecessary Data creation. (#38)](https://github.com/jverkoey/BinaryCodable/commit/49ca3e2430a51af9fd37afb15ee9b902789153e3) (featherless) +* [Use dropFirst instead of subscript notation when reading data. (#37)](https://github.com/jverkoey/BinaryCodable/commit/b1889da65bde47336bf9fa9418cde40863ffb09e) (featherless) +* [Add floating point support. (#23)](https://github.com/jverkoey/BinaryCodable/commit/2e3185ec72a7371ef9402e46b80f161849052ae9) (featherless) +* [Add array coding support + tests. (#14)](https://github.com/jverkoey/BinaryCodable/commit/9a56a79308d1096c31479f1c592b5fa331be0707) (featherless) +* [Drop "Sequential" from the container name. (#12)](https://github.com/jverkoey/BinaryCodable/commit/6b9d1ab11d77f1654dc7ef9c28eec2f52dbccf8f) (featherless) + +## Non-source changes + +* [Update all docs with new container APIs. (#41)](https://github.com/jverkoey/BinaryCodable/commit/c2843b87559d485cbffe7b299fd31e6ac841dc59) (featherless) +* [Update README.md](https://github.com/jverkoey/BinaryCodable/commit/ec5d1f5256f07b004b210fd9b5f5e897030a34cb) (featherless) +* [Update README.md](https://github.com/jverkoey/BinaryCodable/commit/49ce0b41000a92306c326e80333af9a427611b48) (featherless) +* [Add BinaryCookies](https://github.com/jverkoey/BinaryCodable/commit/8f85cba939ed36003389dcc1b3fdfc941bf4a333) (featherless) +* [Add support for embedded types in the proto decoder. (#35)](https://github.com/jverkoey/BinaryCodable/commit/397ccc9bb1dff24bba000b7d42d82a0115a8c74c) (featherless) +* [Add bytes decoding support to the proto decoder. (#34)](https://github.com/jverkoey/BinaryCodable/commit/1328fce67b03b5a41d99730389f21068150d2d4a) (featherless) +* [Add String support to the proto decoder. (#33)](https://github.com/jverkoey/BinaryCodable/commit/07e1c1c3fea8ac4def6fa89639a7c30e9ace217d) (featherless) +* [Add bool support to the proto decoder. (#32)](https://github.com/jverkoey/BinaryCodable/commit/0e93d2224f1a5303ec411015f8ae312aa17beb28) (featherless) +* [Add sint64 support to the proto decoder. (#31)](https://github.com/jverkoey/BinaryCodable/commit/8108735cc28ec8ad1cda549b6b5ce30ca3783427) (featherless) +* [Add uint64 decoding support. (#30)](https://github.com/jverkoey/BinaryCodable/commit/8cbf0bf2f9f7048a1cae01c3f006be3f1a0d533a) (featherless) +* [Add support for optional proto decoding. (#29)](https://github.com/jverkoey/BinaryCodable/commit/b87134d57dc152978bc214f0c7c5a13a3c033144) (featherless) +* [Add double decoding support to the proto decoder. (#28)](https://github.com/jverkoey/BinaryCodable/commit/4c303bfa0d1a6e259ab55674a7afe18409e2ebad) (featherless) +* [Rename the message fields to match the underlying types. (#27)](https://github.com/jverkoey/BinaryCodable/commit/6d937eca935c2eaed901771be83d3c124aa338d8) (featherless) +* [Add proper fixed64 support to the proto decoder. (#26)](https://github.com/jverkoey/BinaryCodable/commit/1c227a1e8f15e709d42cf8cdf0a13221b7a96e49) (featherless) +* [ Add fixed32 support to the proto decoder. (#25)](https://github.com/jverkoey/BinaryCodable/commit/e135e3ca651b91cafe5bcb3ae5babec3935fbad3) (featherless) +* [Add double/fixed64 support to the proto proof of concept. (#24)](https://github.com/jverkoey/BinaryCodable/commit/6e32bee3c8fc98a0ab4b588a0fad0df6226e7769) (featherless) +* [Add a rudimentary Codable-compatible proto decoder built on top of a BinaryCodable message decoder. (#22)](https://github.com/jverkoey/BinaryCodable/commit/21cf2f7c4a7e03f039bd8750ee1e6c91869aecd2) (featherless) +* [Add sint32 proto tests (#21)](https://github.com/jverkoey/BinaryCodable/commit/33f58fc19bbc7ca2bdfd78cc1aee034cdf06607d) (featherless) +* [Add int64 proto tests. (#20)](https://github.com/jverkoey/BinaryCodable/commit/17adab86bedc1981254ac431134a1fd6ed812ab2) (featherless) +* [Add int32 overflow tests to verify the behavior of the protoc compiler. (#19)](https://github.com/jverkoey/BinaryCodable/commit/6d4b7c9eb11643904602c68bfb5079d5e71fe5d2) (featherless) +* [ Add proof of concept protobuf decoder tests (#15)](https://github.com/jverkoey/BinaryCodable/commit/96bf29b4f9fc808c5e1772fe2712f4018e1265ee) (featherless) +* [Update Swift to the latest development snapshot (#13)](https://github.com/jverkoey/BinaryCodable/commit/9cca6e629d352890181d81ce0d2dd47dd2bd6649) (featherless) +* [Update README.md](https://github.com/jverkoey/BinaryCodable/commit/176b7718796104ad22447b4ec86b9e09cb66d8af) (featherless) +* [Fix typo in readme. (#10)](https://github.com/jverkoey/BinaryCodable/commit/ba811ac24e7114628d22792e60620e144b410c88) (featherless) +* [Add missing linux tests. (#9)](https://github.com/jverkoey/BinaryCodable/commit/d2d2c558f0c4d205ff51816965dc3be62fb69a10) (featherless) + +--- + # 0.1.0 This is the first minor, unstable release of BinaryCodable. The public API for this library is subject to change unexpectedly until 1.0.0 is reached, at which point breaking changes will be mitigated and communicated ahead of time. This initial release includes the following features: diff --git a/Docs/ComparisonToSwiftCodable.md b/Docs/ComparisonToSwiftCodable.md index 6da7024..ccce72d 100644 --- a/Docs/ComparisonToSwiftCodable.md +++ b/Docs/ComparisonToSwiftCodable.md @@ -14,11 +14,11 @@ Swift Codable and Binary Codable's related types are outlined below. | `Encodable` | `BinaryEncodable` | | `Decodable` | `BinaryDecodable` | -| Swift Codable Encoder | Binary Codable Encoder | -|:-------------------------------|:------------------------------------| -| `KeyedEncodingContainer` | No equivalent | -| `UnkeyedEncodingContainer` | `SequentialBinaryEncodingContainer` | -| `SingleValueEncodingContainer` | No equivalent | +| Swift Codable Encoder | Binary Codable Encoder | +|:-------------------------------|:--------------------------| +| `KeyedEncodingContainer` | No equivalent | +| `UnkeyedEncodingContainer` | `BinaryEncodingContainer` | +| `SingleValueEncodingContainer` | No equivalent | | Swift Codable Encoding Container | Binary Codable Encoding Container | |:---------------------------------|:--------------------------------------------------------------------------| @@ -34,11 +34,11 @@ Swift Codable and Binary Codable's related types are outlined below. | `encode(_ value: String)` | `encode(_ value: String, encoding: String.Encoding, terminator: UInt8?)` | | No equivalent | `encode(sequence: S) throws where S: Sequence, S.Element == UInt8` | -| Swift Codable Decoder | Binary Codable Decoder | -|:-------------------------------|:------------------------------------| -| `KeyedDecodingContainer` | No equivalent | -| `UnkeyedDecodingContainer` | `SequentialBinaryDecodingContainer` | -| `SingleValueDecodingContainer` | No equivalent | +| Swift Codable Decoder | Binary Codable Decoder | +|:-------------------------------|:--------------------------| +| `KeyedDecodingContainer` | No equivalent | +| `UnkeyedDecodingContainer` | `BinaryDecodingContainer` | +| `SingleValueDecodingContainer` | No equivalent | | Swift Codable Decoding Container | Binary Codable Decoding Container | |:------------------------------------------------|:------------------------------------------------------------------------| diff --git a/Docs/README.md b/Docs/README.md index 9835706..07f4d07 100644 --- a/Docs/README.md +++ b/Docs/README.md @@ -22,8 +22,6 @@ struct GIFHeader { We'll implement decoding first because it's easier to test our code against an existing gif file. -Aside: the biggest distinction between Swift Codable and Binary Codable is that we do not get encoding and decoding implementations for complex types for free. This is presently by design, though there are [opportunities for improving this in the future](https://github.com/jverkoey/BinaryCodable/issues/4). That being said, Binary Codable does provide automatic implementations for RawRepresentable types (namely enums with raw values). - ```swift // New import BinaryCodable @@ -41,6 +39,8 @@ let decoder = BinaryDataDecoder() let header = try decoder.decode(GIFHeader.self, from: data) ``` +Aside: the biggest distinction between Swift Codable and Binary Codable is that we do not get encoding and decoding implementations for complex types for free. This is presently by design, though there are [opportunities for improving this in the future](https://github.com/jverkoey/BinaryCodable/issues/4). That being said, Binary Codable does provide automatic implementations for RawRepresentable types (namely enums with raw values). + Like Swift Codable, we first create a sequential container. Note: the container variable needs to be a `var` because we will mutate it. @@ -52,7 +52,7 @@ struct GIFHeader: BinaryDecodable { init(from decoder: BinaryDecoder) throws { // New // A nil maxLength means we don't know how long this container is. - var container = decoder.sequentialContainer(maxLength: nil) + var container = decoder.container(maxLength: nil) } } ``` @@ -82,7 +82,7 @@ import BinaryCodable struct GIFHeader: BinaryDecodable { init(from decoder: BinaryDecoder) throws { // Modified - var container = decoder.sequentialContainer(maxLength: 13) + var container = decoder.container(maxLength: 13) } } ``` @@ -145,7 +145,7 @@ struct GIFHeader: BinaryDecodable { By using an enum we've accomplished two things: 1. Clearly defined the expected values for this field. -2. Added error handling for unexpected values: if a GIF format version other than 87a or 89a are encountered, a `BinaryDecodingError.dataCorrupted` exception will be thrown. +2. Added error handling for unexpected values: if a GIF format version other than 87a or 89a is encountered, a `BinaryDecodingError.dataCorrupted` exception will be thrown. Note: we can also apply this pattern to `signature` using a single-value String enum. Try cleaning up your implementation accordingly! @@ -279,7 +279,7 @@ struct GIFHeader: BinaryDecodable { let aspectRatio: UInt8 init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: 13) + var container = decoder.container(maxLength: 13) let signature = try container.decode(length: 3) if signature != Data("GIF".utf8) { @@ -351,7 +351,7 @@ import BinaryCodable struct GIFHeader: BinaryCodable { func encode(to encoder: BinaryEncoder) throws { // New - var container = encoder.sequentialContainer() + var container = encoder.container() } } ``` @@ -363,7 +363,7 @@ import BinaryCodable struct GIFHeader: BinaryCodable { func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() // New try container.encode("GIF", encoding: .ascii, terminator: nil) @@ -383,7 +383,7 @@ struct GIFHeader: BinaryCodable { case gif89a = "89a" } func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode("GIF", encoding: .ascii, terminator: nil) // New @@ -399,7 +399,7 @@ import BinaryCodable struct GIFHeader: BinaryCodable { func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode("GIF", encoding: .ascii, terminator: nil) try container.encode(version) @@ -420,7 +420,7 @@ struct GIFHeader: BinaryCodable { struct Packed: OptionSet, BinaryCodable func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode("GIF", encoding: .ascii, terminator: nil) try container.encode(version) @@ -449,7 +449,7 @@ import BinaryCodable struct GIFHeader: BinaryCodable { func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode("GIF", encoding: .ascii, terminator: nil) try container.encode(version) diff --git a/Docs/Usage.md b/Docs/Usage.md index 60e13bf..fa00f11 100644 --- a/Docs/Usage.md +++ b/Docs/Usage.md @@ -10,7 +10,7 @@ import BinaryCodable struct <#DecodableObject#>: BinaryDecodable { init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: <#maxLengthOrNil#>) + var container = decoder.container(maxLength: <#maxLengthOrNil#>) <#decoding logic#> } @@ -31,7 +31,7 @@ import BinaryCodable struct <#EncodableObject#>: BinaryEncodable { func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() <#encoding logic#> } @@ -82,7 +82,7 @@ import BinaryCodable struct <#DecodableObject#>: BinaryDecodable { init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: <#maxLengthOrNil#>) + var container = decoder.container(maxLength: <#maxLengthOrNil#>) let string = try container.decodeString(encoding: .utf8, terminator: 0) } @@ -97,7 +97,7 @@ import BinaryCodable struct <#DecodableObject#>: BinaryDecodable { init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: <#maxLengthOrNil#>) + var container = decoder.container(maxLength: <#maxLengthOrNil#>) var stringContainer = container.nestedContainer(maxLength: <#length#>) let string = try stringContainer.decodeString(encoding: .utf8, terminator: nil) @@ -113,7 +113,7 @@ import BinaryCodable struct <#EncodableObject#>: BinaryEncodable { func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode("String", encoding: .utf8, terminator: 0) } @@ -128,7 +128,7 @@ import BinaryCodable struct <#DecodableObject#>: BinaryDecodable { init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: nil) + var container = decoder.container(maxLength: nil) let data = try container.decode(length: <#T##Int#>) } @@ -143,7 +143,7 @@ import BinaryCodable struct <#EncodableObject#>: BinaryEncodable { func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode(sequence: <#T##Sequence#>) } diff --git a/README.md b/README.md index 7f95db5..8890f2c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Binary Codable -Binary Codable provides Swift Codable-like interfaces for converting types into and from binary representations. +Binary Codable provides Swift Codable-like interfaces for converting types to and from binary representations. Binary Codable is optimized for reading and writing blocks of binary data as a stream of bytes. This makes Binary Codable useful for network protocols, binary file formats, and other forms of tightly-packed binary information. @@ -34,7 +34,8 @@ This is not an official Google product. ## Known usage in the wild -- [MySqlConnector](https://github.com/jverkoey/MySqlConnector): Translates between Swift types and MySql client/server protocol. +- [BinaryCookies](https://github.com/interstateone/BinaryCookies): Read and write Apple's .binarycookies files. +- [MySqlConnector](https://github.com/jverkoey/MySqlConnector): A pure Swift implementation of the MySql client/server protocol. ## Requirements diff --git a/Sources/BinaryCodable/BinaryDataCoders/BinaryDataDecoder.swift b/Sources/BinaryCodable/BinaryDataCoders/BinaryDataDecoder.swift index 3aeb3db..e43bfe5 100644 --- a/Sources/BinaryCodable/BinaryDataCoders/BinaryDataDecoder.swift +++ b/Sources/BinaryCodable/BinaryDataCoders/BinaryDataDecoder.swift @@ -55,7 +55,7 @@ public struct BinaryDataDecoder { private struct _BinaryDataDecoder: BinaryDecoder { var bufferedData: BufferedData let userInfo: [BinaryCodingUserInfoKey: Any] - let container: SequentialBinaryDecodingContainer? + let container: BinaryDataDecodingContainer? init(bufferedData: BufferedData, userInfo: [BinaryCodingUserInfoKey: Any]) { self.bufferedData = bufferedData @@ -63,21 +63,23 @@ private struct _BinaryDataDecoder: BinaryDecoder { self.container = nil } - init(bufferedData: BufferedData, userInfo: [BinaryCodingUserInfoKey: Any], container: SequentialBinaryDecodingContainer) { + init(bufferedData: BufferedData, userInfo: [BinaryCodingUserInfoKey: Any], container: BinaryDataDecodingContainer) { self.bufferedData = bufferedData self.userInfo = userInfo self.container = container } - func sequentialContainer(maxLength: Int?) -> SequentialBinaryDecodingContainer { - if let maxLength = maxLength, let container = container as? BinaryDataDecodingContainer, let remainingLength = container.remainingLength { - return BinaryDataDecodingContainer(bufferedData: bufferedData, - maxLength: min(maxLength, remainingLength), - userInfo: userInfo) - } else if let container = container as? BinaryDataDecodingContainer, let remainingLength = container.remainingLength { - return BinaryDataDecodingContainer(bufferedData: bufferedData, - maxLength: remainingLength, - userInfo: userInfo) + func container(maxLength: Int?) -> BinaryDecodingContainer { + if let remainingLength = container?.remainingLength { + if let maxLength = maxLength { + return BinaryDataDecodingContainer(bufferedData: bufferedData, + maxLength: min(maxLength, remainingLength), + userInfo: userInfo) + } else { + return BinaryDataDecodingContainer(bufferedData: bufferedData, + maxLength: remainingLength, + userInfo: userInfo) + } } else { return BinaryDataDecodingContainer(bufferedData: bufferedData, maxLength: maxLength, userInfo: userInfo) } @@ -85,7 +87,7 @@ private struct _BinaryDataDecoder: BinaryDecoder { } // This needs to be a class instead of a struct because we hold a mutating reference in decode. -private class BinaryDataDecodingContainer: SequentialBinaryDecodingContainer { +private class BinaryDataDecodingContainer: BinaryDecodingContainer { var bufferedData: BufferedData var remainingLength: Int? let userInfo: [BinaryCodingUserInfoKey: Any] @@ -102,8 +104,30 @@ private class BinaryDataDecodingContainer: SequentialBinaryDecodingContainer { return bufferedData.isAtEnd } - func decode(_ type: IntegerType.Type) throws -> IntegerType { - return try decodeFixedWidthInteger(type) + func decode(_ type: T.Type) throws -> T { + let byteWidth = (type.significandBitCount + type.exponentBitCount + 1) / 8 + let bytes = try pullData(length: byteWidth) + if bytes.count < byteWidth { + throw BinaryDecodingError.dataCorrupted(.init(debugDescription: + "Not enough data to create a a type of \(type). Needed: \(byteWidth). Received: \(bytes.count).")) + } + let value = bytes.withUnsafeBytes { (ptr: UnsafePointer) -> T in + return ptr.pointee + } + return value + } + + func decode(_ type: T.Type) throws -> T { + let byteWidth = type.bitWidth / 8 + let bytes = try pullData(length: byteWidth) + if bytes.count < byteWidth { + throw BinaryDecodingError.dataCorrupted(.init(debugDescription: + "Not enough data to create a a type of \(type). Needed: \(byteWidth). Received: \(bytes.count).")) + } + let value = bytes.withUnsafeBytes { (ptr: UnsafePointer) -> T in + return ptr.pointee + } + return value } func decodeString(encoding: String.Encoding, terminator: UInt8?) throws -> String { @@ -141,16 +165,21 @@ private class BinaryDataDecodingContainer: SequentialBinaryDecodingContainer { return data } - func nestedContainer(maxLength: Int?) -> SequentialBinaryDecodingContainer { + func nestedContainer(maxLength: Int?) -> BinaryDecodingContainer { let length: Int? - if let remainingLength = remainingLength, let maxLength = maxLength { - length = min(remainingLength, maxLength) - } else if let remainingLength = remainingLength { - length = remainingLength + let bufferedData: BufferedData + if let remainingLength = remainingLength { + if let maxLength = maxLength { + length = min(remainingLength, maxLength) + } else { + length = remainingLength + } + bufferedData = containedBuffer() } else { length = maxLength + bufferedData = self.bufferedData } - return BinaryDataDecodingContainer(bufferedData: containedBuffer(), maxLength: length, userInfo: userInfo) + return BinaryDataDecodingContainer(bufferedData: bufferedData, maxLength: length, userInfo: userInfo) } func pullData(length: Int) throws -> Data { @@ -182,19 +211,6 @@ private class BinaryDataDecodingContainer: SequentialBinaryDecodingContainer { return data } - func decodeFixedWidthInteger(_ type: T.Type) throws -> T where T: FixedWidthInteger { - let byteWidth = type.bitWidth / 8 - let bytes = try pullData(length: byteWidth) - if bytes.count < byteWidth { - throw BinaryDecodingError.dataCorrupted(.init(debugDescription: - "Not enough data to create a a type of \(type). Needed: \(byteWidth). Received: \(bytes.count).")) - } - let value = Data(bytes).withUnsafeBytes { (ptr: UnsafePointer) -> T in - return ptr.pointee - } - return value - } - private func containedBuffer() -> BufferedData { return BufferedData(reader: AnyBufferedDataSource(read: { recommendedAmount -> Data? in let data = try self.pullData(length: recommendedAmount) diff --git a/Sources/BinaryCodable/BinaryDataCoders/BinaryDataEncoder.swift b/Sources/BinaryCodable/BinaryDataCoders/BinaryDataEncoder.swift index 4945719..7996c49 100644 --- a/Sources/BinaryCodable/BinaryDataCoders/BinaryDataEncoder.swift +++ b/Sources/BinaryCodable/BinaryDataCoders/BinaryDataEncoder.swift @@ -40,18 +40,22 @@ private final class BinaryDataEncoderStorage { private struct _BinaryDataEncoder: BinaryEncoder { var storage = BinaryDataEncoderStorage() - func sequentialContainer() -> SequentialBinaryEncodingContainer { + func container() -> BinaryEncodingContainer { return BinaryDataEncodingContainer(encoder: self) } } -private struct BinaryDataEncodingContainer: SequentialBinaryEncodingContainer { +private struct BinaryDataEncodingContainer: BinaryEncodingContainer { let encoder: _BinaryDataEncoder init(encoder: _BinaryDataEncoder) { self.encoder = encoder } - func encode(_ value: IntegerType) throws { + func encode(_ value: T) throws { + encoder.storage.data.append(contentsOf: value.bytes) + } + + func encode(_ value: T) throws { encoder.storage.data.append(contentsOf: value.bytes) } diff --git a/Sources/BinaryCodable/BinaryDataCoders/BufferedData.swift b/Sources/BinaryCodable/BinaryDataCoders/BufferedData.swift index 2c8f76b..eacb62a 100644 --- a/Sources/BinaryCodable/BinaryDataCoders/BufferedData.swift +++ b/Sources/BinaryCodable/BinaryDataCoders/BufferedData.swift @@ -32,7 +32,7 @@ public final class BufferedData { /** Whether the buffer's internal cursor has reached the end of the available content. */ - public var isAtEnd: Bool = false + public var isAtEnd: Bool { return buffer.count == 0 && reader.isAtEnd } /** - parameter reader: An object that implements mechanisms for retrieving data that can be added to the buffer. @@ -52,7 +52,6 @@ public final class BufferedData { public func peek(maxLength: Int) throws -> Data { while buffer.count < maxLength { guard let data = try reader.read(length: maxLength - buffer.count) else { - isAtEnd = true break } buffer.append(data) @@ -71,13 +70,12 @@ public final class BufferedData { public func read(maxBytes: Int) throws -> Data { while buffer.count < maxBytes { guard let data = try reader.read(length: maxBytes - buffer.count) else { - isAtEnd = true break } buffer.append(data) } let data = buffer.prefix(maxBytes) - buffer = buffer[(buffer.startIndex + data.count)...] + buffer = buffer.dropFirst(data.count) return data } @@ -93,7 +91,6 @@ public final class BufferedData { var indexOfDelimiter = buffer.firstIndex(of: delimiter) while indexOfDelimiter == nil { guard let data = try reader.read(length: 1) else { - isAtEnd = true break } if let subIndex = data.firstIndex(of: delimiter) { @@ -103,12 +100,12 @@ public final class BufferedData { } if let indexOfDelimiter = indexOfDelimiter { let data = buffer.prefix(indexOfDelimiter - buffer.startIndex) - buffer = buffer[(buffer.startIndex + data.count + 1)...] + buffer = buffer.dropFirst(data.count + 1) return (data: data, didFindDelimiter: true) } else { // Couldn't find the delimeter, so read in all of the data. let data = buffer - buffer = buffer[(buffer.startIndex + data.count)...] + buffer = buffer.dropFirst(data.count) return (data: data, didFindDelimiter: false) } } @@ -171,8 +168,8 @@ private final class DataBufferedDataSource: BufferedDataSource { guard !isAtEnd else { return nil } - let requestedData = data.prefix(length) - data = data.dropFirst(length) + let requestedData = data + data.removeAll() return requestedData } diff --git a/Sources/BinaryCodable/BinaryDataCoders/FixedWidthInteger+bytes.swift b/Sources/BinaryCodable/BinaryDataCoders/ByteRepresentations.swift similarity index 77% rename from Sources/BinaryCodable/BinaryDataCoders/FixedWidthInteger+bytes.swift rename to Sources/BinaryCodable/BinaryDataCoders/ByteRepresentations.swift index 6bf43eb..198d144 100644 --- a/Sources/BinaryCodable/BinaryDataCoders/FixedWidthInteger+bytes.swift +++ b/Sources/BinaryCodable/BinaryDataCoders/ByteRepresentations.swift @@ -23,3 +23,14 @@ extension FixedWidthInteger { return stride(from: 0, to: bitWidth, by: 8).map { UInt8((self >> $0) & 0xFF) } } } + +extension BinaryFloatingPoint { + /** + Returns a byte array representation of a floating point number. + */ + public var bytes: [UInt8] { + var value = self + let data = Data(buffer: UnsafeBufferPointer(start: &value, count: 1)) + return [UInt8](data) + } +} diff --git a/Sources/BinaryCodable/BinaryDecodable.swift b/Sources/BinaryCodable/BinaryDecodable.swift index 1818dd9..8f41dce 100644 --- a/Sources/BinaryCodable/BinaryDecodable.swift +++ b/Sources/BinaryCodable/BinaryDecodable.swift @@ -46,7 +46,7 @@ public protocol BinaryDecoder { this container is able to read an infinite number of bytes. - returns: A container view into this decoder. */ - func sequentialContainer(maxLength: Int?) -> SequentialBinaryDecodingContainer + func container(maxLength: Int?) -> BinaryDecodingContainer } /** @@ -86,7 +86,7 @@ public enum BinaryDecodingError: Error { A type that provides a view into a decoder's storage and is used to hold the encoded properties of a decodable type sequentially. */ -public protocol SequentialBinaryDecodingContainer { +public protocol BinaryDecodingContainer { /** A Boolean value indicating whether there are no more elements left to be decoded in the container. @@ -109,7 +109,15 @@ public protocol SequentialBinaryDecodingContainer { - parameter type: The type of value to decode. - returns: A value of the requested type. */ - mutating func decode(_ type: IntegerType.Type) throws -> IntegerType + mutating func decode(_ type: T.Type) throws -> T + + /** + Decodes a value of the given type. + + - parameter type: The type of value to decode. + - returns: A value of the requested type. + */ + mutating func decode(_ type: T.Type) throws -> T /** Decodes a value of the given type. @@ -142,16 +150,26 @@ public protocol SequentialBinaryDecodingContainer { returned container is able to read all bytes that are readable by this container. - returns: A decoding container view into `self`. */ - mutating func nestedContainer(maxLength: Int?) -> SequentialBinaryDecodingContainer + mutating func nestedContainer(maxLength: Int?) -> BinaryDecodingContainer } // MARK: RawRepresentable extensions -// Primarily for enums with some FixedWidthInteger raw value extension RawRepresentable where RawValue: FixedWidthInteger, Self: BinaryDecodable { public init(from decoder: BinaryDecoder) throws { - let byteWidth = RawValue.bitWidth / 8 - var container = decoder.sequentialContainer(maxLength: byteWidth) + var container = decoder.container(maxLength: nil) + let decoded = try container.decode(RawValue.self) + guard let value = Self(rawValue: decoded) else { + throw BinaryDecodingError.dataCorrupted(.init(debugDescription: + "Cannot initialize \(Self.self) from invalid \(RawValue.self) value \(decoded)")) + } + self = value + } +} + +extension RawRepresentable where RawValue: BinaryFloatingPoint, Self: BinaryDecodable { + public init(from decoder: BinaryDecoder) throws { + var container = decoder.container(maxLength: nil) let decoded = try container.decode(RawValue.self) guard let value = Self(rawValue: decoded) else { throw BinaryDecodingError.dataCorrupted(.init(debugDescription: @@ -161,10 +179,9 @@ extension RawRepresentable where RawValue: FixedWidthInteger, Self: BinaryDecoda } } -// Primarily for enums with a String raw value extension RawRepresentable where RawValue == String, Self: BinaryDecodable { public init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: nil) + var container = decoder.container(maxLength: nil) let decoded = try container.decodeString(encoding: .utf8, terminator: nil) guard let value = Self(rawValue: decoded) else { throw BinaryDecodingError.dataCorrupted(.init(debugDescription: @@ -173,3 +190,15 @@ extension RawRepresentable where RawValue == String, Self: BinaryDecodable { self = value } } + +extension Array: BinaryDecodable where Element: BinaryDecodable { + public init(from decoder: BinaryDecoder) throws { + self.init() + + var container = decoder.container(maxLength: nil) + while !container.isAtEnd { + let element = try container.decode(Element.self) + append(element) + } + } +} diff --git a/Sources/BinaryCodable/BinaryEncodable.swift b/Sources/BinaryCodable/BinaryEncodable.swift index b5dfae0..bbafbee 100644 --- a/Sources/BinaryCodable/BinaryEncodable.swift +++ b/Sources/BinaryCodable/BinaryEncodable.swift @@ -38,7 +38,7 @@ public protocol BinaryEncoder { - returns: A new empty container. */ - func sequentialContainer() -> SequentialBinaryEncodingContainer + func container() -> BinaryEncodingContainer } /** @@ -78,7 +78,7 @@ public enum BinaryEncodingError: Error { A type that provides a view into an encoder's storage and is used to hold the encoded properties of a encodable type sequentially. */ -public protocol SequentialBinaryEncodingContainer { +public protocol BinaryEncodingContainer { /** Encodes a String value using the given encoding and with a terminator at the end. @@ -91,11 +91,18 @@ public protocol SequentialBinaryEncodingContainer { mutating func encode(_ value: String, encoding: String.Encoding, terminator: UInt8?) throws /** - Encodes a value of the given type. + Encodes a value of the given integer type. + + - parameter value: The value to encode. + */ + mutating func encode(_ value: T) throws + + /** + Encodes a value of the given floating point type. - parameter value: The value to encode. */ - mutating func encode(_ value: IntegerType) throws + mutating func encode(_ value: T) throws /** Encodes a value of the given type. @@ -116,14 +123,30 @@ public protocol SequentialBinaryEncodingContainer { extension RawRepresentable where RawValue: FixedWidthInteger, Self: BinaryEncodable { public func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() + try container.encode(self.rawValue) + } +} + +extension RawRepresentable where RawValue: BinaryFloatingPoint, Self: BinaryEncodable { + public func encode(to encoder: BinaryEncoder) throws { + var container = encoder.container() try container.encode(self.rawValue) } } extension RawRepresentable where RawValue == String, Self: BinaryEncodable { public func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode(self.rawValue, encoding: .utf8, terminator: nil) } } + +extension Array: BinaryEncodable where Element: BinaryEncodable { + public func encode(to encoder: BinaryEncoder) throws { + var container = encoder.container() + for element in self { + try container.encode(element) + } + } +} diff --git a/Tests/BinaryCodableTests/ArrayDecoderTests.swift b/Tests/BinaryCodableTests/ArrayDecoderTests.swift new file mode 100644 index 0000000..2dd4cdb --- /dev/null +++ b/Tests/BinaryCodableTests/ArrayDecoderTests.swift @@ -0,0 +1,64 @@ +// Copyright 2019-present the BinaryCodable authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import BinaryCodable +import XCTest + +private struct Packet: BinaryDecodable { + let data: Data + init(from decoder: BinaryDecoder) throws { + var container = decoder.container(maxLength: nil) + let payloadLength = try container.decode(UInt32.self) + self.data = try container.decode(length: Int(payloadLength)) + } +} + +final class ArrayDecoderTests: XCTestCase { + + func testEmpty() throws { + // Given + let packetData: [UInt8] = [0, 0, 0, 0, 0, 0, 0, 0] + let decoder = BinaryDataDecoder() + + // When + let packets = try decoder.decode([Packet].self, from: packetData) + + // Then + XCTAssertEqual(packets.map { [UInt8]($0.data) }, [[], []]) + } + + func testOneByte() throws { + // Given + let packetData: [UInt8] = [1, 0, 0, 0, 127, 1, 0, 0, 0, 32] + let decoder = BinaryDataDecoder() + + // When + let packets = try decoder.decode([Packet].self, from: packetData) + + // Then + XCTAssertEqual(packets.map { [UInt8]($0.data) }, [[127], [32]]) + } + + func testMultipleByte() throws { + // Given + let packetData: [UInt8] = [5, 0, 0, 0, 127, 32, 48, 12, 10, 4, 0, 0, 0, 10, 15, 0, 255] + let decoder = BinaryDataDecoder() + + // When + let packets = try decoder.decode([Packet].self, from: packetData) + + // Then + XCTAssertEqual(packets.map { [UInt8]($0.data) }, [[127, 32, 48, 12, 10], [10, 15, 0, 255]]) + } +} diff --git a/Tests/BinaryCodableTests/ArrayEncoderTests.swift b/Tests/BinaryCodableTests/ArrayEncoderTests.swift new file mode 100644 index 0000000..1e49c05 --- /dev/null +++ b/Tests/BinaryCodableTests/ArrayEncoderTests.swift @@ -0,0 +1,65 @@ +// Copyright 2019-present the BinaryCodable authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import BinaryCodable +import XCTest + +private struct Packet: BinaryEncodable { + let data: Data + + func encode(to encoder: BinaryEncoder) throws { + var container = encoder.container() + try container.encode(UInt32(data.count)) + try container.encode(sequence: data) + } +} + +final class ArrayEncoderTests: XCTestCase { + + func testEmptyLength() throws { + // Given + let packets = [Packet(data: Data([])), Packet(data: Data([]))] + let encoder = BinaryDataEncoder() + + // When + let packetData = try encoder.encode(packets) + + // Then + XCTAssertEqual([UInt8](packetData), [0, 0, 0, 0, 0, 0, 0, 0]) + } + + func testOneByte() throws { + // Given + let packets = [Packet(data: Data([127])), Packet(data: Data([32]))] + let encoder = BinaryDataEncoder() + + // When + let packetData = try encoder.encode(packets) + + // Then + XCTAssertEqual([UInt8](packetData), [1, 0, 0, 0, 127, 1, 0, 0, 0, 32]) + } + + func testMultipleByte() throws { + // Given + let packets = [Packet(data: Data([127, 32, 48, 12, 10])), Packet(data: Data([10, 15, 0, 255]))] + let encoder = BinaryDataEncoder() + + // When + let packetData = try encoder.encode(packets) + + // Then + XCTAssertEqual([UInt8](packetData), [5, 0, 0, 0, 127, 32, 48, 12, 10, 4, 0, 0, 0, 10, 15, 0, 255]) + } +} diff --git a/Tests/BinaryCodableTests/BinaryFloatingPoint+bytes.swift b/Tests/BinaryCodableTests/BinaryFloatingPoint+bytes.swift new file mode 100644 index 0000000..5ca51f3 --- /dev/null +++ b/Tests/BinaryCodableTests/BinaryFloatingPoint+bytes.swift @@ -0,0 +1,41 @@ +// Copyright 2019-present the BinaryCodable authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import XCTest +@testable import BinaryCodable + +final class BinaryFloatingPointBytesTests: XCTestCase { + + func testFloatIsFourBytes() { + // Given + let value: Float = 3.14159 + + // When + let bytes = value.bytes + + // Then + XCTAssertEqual(bytes.count, 4) + } + + func testDoubleIsFourBytes() { + // Given + let value: Double = 3.14159 + + // When + let bytes = value.bytes + + // Then + XCTAssertEqual(bytes.count, 8) + } +} diff --git a/Tests/BinaryCodableTests/BufferedDataTests.swift b/Tests/BinaryCodableTests/BufferedDataTests.swift index b5d4b6b..09df4c6 100644 --- a/Tests/BinaryCodableTests/BufferedDataTests.swift +++ b/Tests/BinaryCodableTests/BufferedDataTests.swift @@ -20,40 +20,43 @@ final class BufferedDataTests: XCTestCase { func testInitiallyPullsFromStart() throws { // Given let data = Data([UInt8](0..<255)) - let lazyData = bufferedData(from: data) + let buffer = bufferedData(from: data) // When - let readData = try lazyData.read(maxBytes: 100) + let readData = try buffer.read(maxBytes: 100) // Then XCTAssertEqual([UInt8](readData), [UInt8](0..<100)) + XCTAssertFalse(buffer.isAtEnd) } func testSuccessivePullsUseCursor() throws { // Given let data = Data([UInt8](0..<255)) - let lazyData = bufferedData(from: data) + let buffer = bufferedData(from: data) // When - let readData1 = try lazyData.read(maxBytes: 25) - let readData2 = try lazyData.read(maxBytes: 25) - let readData3 = try lazyData.read(maxBytes: 25) + let readData1 = try buffer.read(maxBytes: 25) + let readData2 = try buffer.read(maxBytes: 25) + let readData3 = try buffer.read(maxBytes: 25) // Then XCTAssertEqual([UInt8](readData1), [UInt8](0..<25)) XCTAssertEqual([UInt8](readData2), [UInt8](25..<50)) XCTAssertEqual([UInt8](readData3), [UInt8](50..<75)) + XCTAssertFalse(buffer.isAtEnd) } func testPullingMoreThanAvailableOnlyPullsWhatsAvailable() throws { // Given let data = Data([UInt8](0..<255)) - let lazyData = bufferedData(from: data) + let buffer = bufferedData(from: data) // When - let readData = try lazyData.read(maxBytes: 1000) + let readData = try buffer.read(maxBytes: 1000) // Then XCTAssertEqual([UInt8](readData), [UInt8](0..<255)) + XCTAssertTrue(buffer.isAtEnd) } } diff --git a/Tests/BinaryCodableTests/FixedWidthInteger+bytes.swift b/Tests/BinaryCodableTests/FixedWidthInteger+bytes.swift index cfb9f00..4c53822 100644 --- a/Tests/BinaryCodableTests/FixedWidthInteger+bytes.swift +++ b/Tests/BinaryCodableTests/FixedWidthInteger+bytes.swift @@ -15,7 +15,7 @@ import XCTest @testable import BinaryCodable -final class Tests: XCTestCase { +final class FixedWidthIntegerBytesTests: XCTestCase { func testUInt8IsOneByte() { // Given diff --git a/Tests/BinaryCodableTests/GIFHeaderTests.swift b/Tests/BinaryCodableTests/GIFHeaderTests.swift index 8cbd565..cd298e0 100644 --- a/Tests/BinaryCodableTests/GIFHeaderTests.swift +++ b/Tests/BinaryCodableTests/GIFHeaderTests.swift @@ -67,7 +67,7 @@ struct GIFHeader: BinaryCodable { } init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: 13) + var container = decoder.container(maxLength: 13) let signature = try container.decode(length: 3) if signature != Data("GIF".utf8) { @@ -92,7 +92,7 @@ struct GIFHeader: BinaryCodable { } func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode("GIF", encoding: .ascii, terminator: nil) try container.encode(version) diff --git a/Tests/BinaryCodableTests/BinaryDataDecoderTests.swift b/Tests/BinaryCodableTests/LengthEncodedPacketDecoderTests.swift similarity index 93% rename from Tests/BinaryCodableTests/BinaryDataDecoderTests.swift rename to Tests/BinaryCodableTests/LengthEncodedPacketDecoderTests.swift index c9a2405..d57583c 100644 --- a/Tests/BinaryCodableTests/BinaryDataDecoderTests.swift +++ b/Tests/BinaryCodableTests/LengthEncodedPacketDecoderTests.swift @@ -18,13 +18,13 @@ import XCTest private struct Packet: BinaryDecodable { let data: Data init(from decoder: BinaryDecoder) throws { - var container = decoder.sequentialContainer(maxLength: nil) + var container = decoder.container(maxLength: nil) let payloadLength = try container.decode(UInt32.self) self.data = try container.decode(length: Int(payloadLength)) } } -final class BinaryDataDecoderTests: XCTestCase { +final class LengthEncodedPacketDecoderTests: XCTestCase { func testEmpty() throws { // Given diff --git a/Tests/BinaryCodableTests/BinaryDataEncoderTests.swift b/Tests/BinaryCodableTests/LengthEncodedPacketEncoderTests.swift similarity index 94% rename from Tests/BinaryCodableTests/BinaryDataEncoderTests.swift rename to Tests/BinaryCodableTests/LengthEncodedPacketEncoderTests.swift index d13260f..f02dde6 100644 --- a/Tests/BinaryCodableTests/BinaryDataEncoderTests.swift +++ b/Tests/BinaryCodableTests/LengthEncodedPacketEncoderTests.swift @@ -19,13 +19,13 @@ private struct Packet: BinaryEncodable { let data: Data func encode(to encoder: BinaryEncoder) throws { - var container = encoder.sequentialContainer() + var container = encoder.container() try container.encode(UInt32(data.count)) try container.encode(sequence: data) } } -final class BinaryDataEncoderTests: XCTestCase { +final class LengthEncodedPacketEncoderTests: XCTestCase { func testEmptyLength() throws { // Given diff --git a/Tests/BinaryCodableTests/XCTestManifests.swift b/Tests/BinaryCodableTests/XCTestManifests.swift index 8de7e05..ff8f757 100644 --- a/Tests/BinaryCodableTests/XCTestManifests.swift +++ b/Tests/BinaryCodableTests/XCTestManifests.swift @@ -1,6 +1,6 @@ import XCTest -extension BinaryDataDecoderTests { +extension ArrayDecoderTests { static let __allTests = [ ("testEmpty", testEmpty), ("testMultipleByte", testMultipleByte), @@ -8,7 +8,7 @@ extension BinaryDataDecoderTests { ] } -extension BinaryDataEncoderTests { +extension ArrayEncoderTests { static let __allTests = [ ("testEmptyLength", testEmptyLength), ("testMultipleByte", testMultipleByte), @@ -16,6 +16,13 @@ extension BinaryDataEncoderTests { ] } +extension BinaryFloatingPointBytesTests { + static let __allTests = [ + ("testDoubleIsFourBytes", testDoubleIsFourBytes), + ("testFloatIsFourBytes", testFloatIsFourBytes), + ] +} + extension BufferedDataTests { static let __allTests = [ ("testInitiallyPullsFromStart", testInitiallyPullsFromStart), @@ -24,7 +31,7 @@ extension BufferedDataTests { ] } -extension Tests { +extension FixedWidthIntegerBytesTests { static let __allTests = [ ("testIntDependsOnThePlatform", testIntDependsOnThePlatform), ("testUInt16IsTwoBytesInLittleEndian", testUInt16IsTwoBytesInLittleEndian), @@ -34,13 +41,66 @@ extension Tests { ] } +extension GIFDecoderTests { + static let __allTests = [ + ("testDecoding", testDecoding), + ("testEncoding", testEncoding), + ] +} + +extension LengthEncodedPacketDecoderTests { + static let __allTests = [ + ("testEmpty", testEmpty), + ("testMultipleByte", testMultipleByte), + ("testOneByte", testOneByte), + ] +} + +extension LengthEncodedPacketEncoderTests { + static let __allTests = [ + ("testEmptyLength", testEmptyLength), + ("testMultipleByte", testMultipleByte), + ("testOneByte", testOneByte), + ] +} + +extension ProtobufTests { + static let __allTests = [ + ("testDouble0Decoding", testDouble0Decoding), + ("testDoubleValueDecoding", testDoubleValueDecoding), + ("testFixed320Decoding", testFixed320Decoding), + ("testFixed32ValueDecoding", testFixed32ValueDecoding), + ("testFloat0Decoding", testFloat0Decoding), + ("testFloatValueDecoding", testFloatValueDecoding), + ("testGeneratedMessageDecoding", testGeneratedMessageDecoding), + ("testInt320Decoding", testInt320Decoding), + ("testInt32NegativeValueDecoding", testInt32NegativeValueDecoding), + ("testInt32OverflowFailsToCompile", testInt32OverflowFailsToCompile), + ("testInt32PositiveValueDecoding", testInt32PositiveValueDecoding), + ("testInt640Decoding", testInt640Decoding), + ("testInt64NegativeValueDecoding", testInt64NegativeValueDecoding), + ("testInt64PositiveValueDecoding", testInt64PositiveValueDecoding), + ("testMultipleInt32Decoding", testMultipleInt32Decoding), + ("testProtoCompiler", testProtoCompiler), + ("testProtoCompilerPipeline", testProtoCompilerPipeline), + ("testSInt320Decoding", testSInt320Decoding), + ("testSInt32NegativeValueDecoding", testSInt32NegativeValueDecoding), + ("testSInt32PositiveValueDecoding", testSInt32PositiveValueDecoding), + ] +} + #if !os(macOS) public func __allTests() -> [XCTestCaseEntry] { return [ - testCase(BinaryDataDecoderTests.__allTests), - testCase(BinaryDataEncoderTests.__allTests), + testCase(ArrayDecoderTests.__allTests), + testCase(ArrayEncoderTests.__allTests), + testCase(BinaryFloatingPointBytesTests.__allTests), testCase(BufferedDataTests.__allTests), - testCase(Tests.__allTests), + testCase(FixedWidthIntegerBytesTests.__allTests), + testCase(GIFDecoderTests.__allTests), + testCase(LengthEncodedPacketDecoderTests.__allTests), + testCase(LengthEncodedPacketEncoderTests.__allTests), + testCase(ProtobufTests.__allTests), ] } #endif diff --git a/Tests/BinaryCodableTests/protobufs/Message.swift b/Tests/BinaryCodableTests/protobufs/Message.swift new file mode 100644 index 0000000..7f9cae1 --- /dev/null +++ b/Tests/BinaryCodableTests/protobufs/Message.swift @@ -0,0 +1,71 @@ +// Copyright 2019-present the BinaryCodable authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import BinaryCodable +import Foundation + +struct Embedded: ProtoDecodable { + var int32Value: Int32? + + static func fieldDescriptor(for key: CodingKey) -> Field? { + guard let codingKey = key as? CodingKeys else { + return nil + } + switch codingKey { + case .int32Value: return Field(number: 1, type: .int32) + } + } +} + +struct Message: ProtoDecodable { + var doubleValue: Double? + var floatValue: Float? + var int32Value: Int32? + var int64Value: Int64? + var uint32Value: UInt32? + var uint64Value: UInt64? + var sint32Value: Int32? + var sint64Value: Int64? + var fixed32Value: UInt32? + var fixed64Value: UInt64? + var boolValue: Bool? + var stringValue: String? + var bytesValue: Data? + var embedded: Embedded? + + var missingValue: Int32? + + static func fieldDescriptor(for key: CodingKey) -> Field? { + guard let codingKey = key as? CodingKeys else { + return nil + } + switch codingKey { + case .doubleValue: return Field(number: 1, type: .double) + case .floatValue: return Field(number: 2, type: .float) + case .int32Value: return Field(number: 3, type: .int32) + case .int64Value: return Field(number: 4, type: .int64) + case .uint32Value: return Field(number: 5, type: .uint32) + case .uint64Value: return Field(number: 6, type: .uint64) + case .sint32Value: return Field(number: 7, type: .sint32) + case .sint64Value: return Field(number: 8, type: .sint64) + case .fixed32Value: return Field(number: 9, type: .fixed32) + case .fixed64Value: return Field(number: 10, type: .fixed64) + case .boolValue: return Field(number: 13, type: .bool) + case .stringValue: return Field(number: 14, type: .string) + case .bytesValue: return Field(number: 15, type: .bytes) + case .embedded: return Field(number: 16, type: .embedded(Embedded.self)) + case .missingValue: return Field(number: 20, type: .int32) + } + } +} diff --git a/Tests/BinaryCodableTests/protobufs/ProtoDecoderSupport.swift b/Tests/BinaryCodableTests/protobufs/ProtoDecoderSupport.swift new file mode 100644 index 0000000..35398ee --- /dev/null +++ b/Tests/BinaryCodableTests/protobufs/ProtoDecoderSupport.swift @@ -0,0 +1,287 @@ +// Copyright 2019-present the BinaryCodable authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import BinaryCodable +import Foundation + +struct Field { + let number: Int + enum FieldType { + case float + case double + case int32 + case int64 + case uint32 + case uint64 + case sint32 + case sint64 + case fixed32 + case fixed64 + case bool + case string + case bytes + case embedded(ProtoDecodable.Type) + } + let type: FieldType +} +typealias FieldDescriptors = [String: Field] + +protocol ProtoDecodable: Codable { + static func fieldDescriptor(for key: CodingKey) -> Field? +} + +/** + This is a hypothetical generated protobuf decoder that supports Swift's Codable interfaces. + */ +struct ProtoDecoder { + + func decode(_ type: T.Type, from data: Data) throws -> T { + let decoder = try _ProtoDecoder(data: data, fieldDescriptor: T.fieldDescriptor) + return try T.init(from: decoder) + } +} + +private struct _ProtoDecoder: Decoder { + var codingPath: [CodingKey] = [] + var userInfo: [CodingUserInfoKey : Any] = [:] + let mappedMessages: [Int: ProtoMessage] + let fieldDescriptor: (CodingKey) -> Field? + + init(data: Data, fieldDescriptor: @escaping (CodingKey) -> Field?) throws { + let decoder = BinaryDataDecoder() + var mappedMessages: [Int: ProtoMessage] = [:] + for message in try decoder.decode([ProtoMessage].self, from: data) { + mappedMessages[Int(message.fieldNumber)] = message + } + self.mappedMessages = mappedMessages + self.fieldDescriptor = fieldDescriptor + } + + func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer where Key : CodingKey { + let container = ProtoKeyedDecodingContainer(decoder: self) + return KeyedDecodingContainer(container) + } + + func unkeyedContainer() throws -> UnkeyedDecodingContainer { + preconditionFailure("Unimplemented") + } + + func singleValueContainer() throws -> SingleValueDecodingContainer { + preconditionFailure("Unimplemented") + } +} + +private struct ProtoKeyedDecodingContainer: KeyedDecodingContainerProtocol { + var codingPath: [CodingKey] = [] + var allKeys: [Key] = [] + + let decoder: _ProtoDecoder + init(decoder: _ProtoDecoder) { + self.decoder = decoder + } + + func contains(_ key: Key) -> Bool { + guard let fieldDescriptor = decoder.fieldDescriptor(key) else { + return false + } + return decoder.mappedMessages[fieldDescriptor.number] != nil + } + + func decodeNil(forKey key: Key) throws -> Bool { + guard let fieldDescriptor = decoder.fieldDescriptor(key) else { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, + debugDescription: "No field descriptor provided for \(key).")) + } + return decoder.mappedMessages[fieldDescriptor.number] == nil + } + + func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + return try decodeFixedWidthInteger(UInt8.self, forKey: key) != 0 + } + + func decode(_ type: String.Type, forKey key: Key) throws -> String { + guard let fieldDescriptor = decoder.fieldDescriptor(key) else { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, + debugDescription: "No field descriptor provided for \(key).")) + } + guard let message = decoder.mappedMessages[fieldDescriptor.number] else { + throw DecodingError.valueNotFound(type, .init(codingPath: codingPath, + debugDescription: "No value found for \(key) of type \(type).")) + } + switch (fieldDescriptor.type, message.value) { + case (.string, .lengthDelimited(let data)): + guard let string = String(data: data, encoding: .utf8) else { + throw DecodingError.dataCorruptedError(forKey: key, in: self, + debugDescription: "Unable to decode data as a utf8 string: \(data)") + } + return string + default: + preconditionFailure("Unimplemented") + } + } + + func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + guard let fieldDescriptor = decoder.fieldDescriptor(key) else { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, + debugDescription: "No field descriptor provided for \(key).")) + } + guard let message = decoder.mappedMessages[fieldDescriptor.number] else { + throw DecodingError.valueNotFound(type, .init(codingPath: codingPath, + debugDescription: "No value found for \(key) of type \(type).")) + } + switch (fieldDescriptor.type, message.value) { + case (.double, .fixed64(let value)): + return Double(bitPattern: value) + default: + preconditionFailure("Unimplemented \(key) \(fieldDescriptor) \(message)") + } + } + + func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + guard let fieldDescriptor = decoder.fieldDescriptor(key) else { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, + debugDescription: "No field descriptor provided for \(key).")) + } + guard let message = decoder.mappedMessages[fieldDescriptor.number] else { + throw DecodingError.valueNotFound(type, .init(codingPath: codingPath, + debugDescription: "No value found for \(key) of type \(type).")) + } + switch (fieldDescriptor.type, message.value) { + case (.float, .fixed32(let value)): + return Float(bitPattern: value) + default: + preconditionFailure("Unimplemented") + } + } + + func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + return try decodeFixedWidthInteger(type, forKey: key) + } + + private func decodeFixedWidthInteger(_ type: T.Type, forKey key: Key) throws -> T { + guard let fieldDescriptor = decoder.fieldDescriptor(key) else { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, + debugDescription: "No field descriptor provided for \(key).")) + } + guard let message = decoder.mappedMessages[fieldDescriptor.number] else { + throw DecodingError.valueNotFound(T.self, .init(codingPath: codingPath, + debugDescription: "No value found for \(key) of type \(type).")) + } + switch (fieldDescriptor.type, message.value) { + case (.int32, .varint(let rawValue)), + (.int64, .varint(let rawValue)), + (.uint32, .varint(let rawValue)), + (.uint64, .varint(let rawValue)), + (.bool, .varint(let rawValue)): return T.init(clamping: rawValue) + case (.sint32, .varint(let rawValue)): return T.init(clamping: Int32(rawValue >> 1) ^ -Int32(rawValue & 1)) + case (.sint64, .varint(let rawValue)): return T.init(clamping: Int64(rawValue >> 1) ^ -Int64(rawValue & 1)) + case (.fixed32, .fixed32(let rawValue)): return T.init(clamping: rawValue) + case (.fixed64, .fixed64(let rawValue)): return T.init(clamping: rawValue) + default: + preconditionFailure("Unimplemented") + } + } + + func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { + if type == Data.self { + // Data's default Decodable implementation requests an unkeyed container which is not a particularly helpful layer + // of indirection in our case. Rather than fall through to this logic, we handle Data decoding as an exception + // here. + guard let fieldDescriptor = decoder.fieldDescriptor(key) else { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, + debugDescription: "No field descriptor provided for \(key).")) + } + guard let message = decoder.mappedMessages[fieldDescriptor.number] else { + throw DecodingError.valueNotFound(T.self, .init(codingPath: codingPath, + debugDescription: "No value found for \(key) of type \(type).")) + } + switch (fieldDescriptor.type, message.value) { + case (.bytes, .lengthDelimited(let data)): return data as! T + default: + preconditionFailure("Unimplemented") + } + } + + // Embedded types. + guard let fieldDescriptor = self.decoder.fieldDescriptor(key) else { + throw DecodingError.keyNotFound(key, .init(codingPath: codingPath, + debugDescription: "No field descriptor provided for \(key).")) + } + guard let message = self.decoder.mappedMessages[fieldDescriptor.number] else { + throw DecodingError.valueNotFound(T.self, .init(codingPath: codingPath, + debugDescription: "No value found for \(key) of type \(type).")) + } + switch (fieldDescriptor.type, message.value) { + case (.embedded(let embeddedType), .lengthDelimited(let data)): + let decoder = try _ProtoDecoder(data: data, fieldDescriptor: embeddedType.fieldDescriptor) + return try T.init(from: decoder) + default: + preconditionFailure("Unimplemented") + } + } + + func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer where NestedKey : CodingKey { + preconditionFailure("Unimplemented") + } + + func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + preconditionFailure("Unimplemented") + } + + func superDecoder() throws -> Decoder { + preconditionFailure("Unimplemented") + } + + func superDecoder(forKey key: Key) throws -> Decoder { + preconditionFailure("Unimplemented") + } + + +} diff --git a/Tests/BinaryCodableTests/protobufs/ProtoMessage.swift b/Tests/BinaryCodableTests/protobufs/ProtoMessage.swift new file mode 100644 index 0000000..b16194d --- /dev/null +++ b/Tests/BinaryCodableTests/protobufs/ProtoMessage.swift @@ -0,0 +1,93 @@ +// Copyright 2019-present the BinaryCodable authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import BinaryCodable +import Foundation + +/** + This is a hypothetical binary codable protobuf implementation. + + Documentation: https://developers.google.com/protocol-buffers/docs/encoding + */ +struct ProtoMessage: BinaryDecodable, Equatable { + let fieldNumber: UInt64 + enum Value: Equatable { + case varint(rawValue: UInt64) + case lengthDelimited(data: Data) + case fixed32(rawValue: UInt32) + case fixed64(rawValue: UInt64) + } + let value: Value + + init(fieldNumber: UInt64, value: Value) { + self.fieldNumber = fieldNumber + self.value = value + } + + init(from decoder: BinaryDecoder) throws { + var container = decoder.container(maxLength: nil) + + let key = try container.decode(VarInt.self) + + let lowerBits = UInt8(key.rawValue & 0b00000111) + guard let wireType = ValueType(rawValue: lowerBits) else { + throw BinaryDecodingError.dataCorrupted(.init(debugDescription: "Unknown value type \(lowerBits)")) + } + + self.fieldNumber = key.rawValue >> 3 + + switch wireType { + case .varint: self.value = try .varint(rawValue: container.decode(VarInt.self).rawValue) + case .lengthDelimited: self.value = try .lengthDelimited(data: container.decode(LengthDelimited.self).data) + case .fixed32: self.value = try .fixed32(rawValue: container.decode(UInt32.self)) + case .fixed64: self.value = try .fixed64(rawValue: container.decode(UInt64.self)) + } + } + + private enum ValueType: UInt8 { + case varint = 0 + case fixed64 = 1 + case lengthDelimited = 2 + case fixed32 = 5 + } +} + +struct VarInt: BinaryDecodable { + let rawValue: UInt64 + init(from decoder: BinaryDecoder) throws { + var container = decoder.container(maxLength: nil) + + let msb = UInt8(0b10000000) + let lowerbits = UInt8(0b01111111) + var value: UInt64 = 0 + var byte: UInt8 + var shiftAmount: Int = 0 + repeat { + byte = try container.decode(UInt8.self) + value |= UInt64(byte & lowerbits) << shiftAmount + shiftAmount += 7 + } while (byte & msb) == msb + self.rawValue = value + } +} + +struct LengthDelimited: BinaryDecodable { + let data: Data + init(from decoder: BinaryDecoder) throws { + var container = decoder.container(maxLength: nil) + + let length = try container.decode(VarInt.self) + self.data = try container.decode(length: Int(length.rawValue)) + } +} diff --git a/Tests/BinaryCodableTests/protobufs/ProtobufTests.swift b/Tests/BinaryCodableTests/protobufs/ProtobufTests.swift new file mode 100644 index 0000000..baf4793 --- /dev/null +++ b/Tests/BinaryCodableTests/protobufs/ProtobufTests.swift @@ -0,0 +1,733 @@ +// Copyright 2019-present the BinaryCodable authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import BinaryCodable +import Foundation +import XCTest + +class ProtobufTests: XCTestCase { + private let environment = TestConfig.environment + + func testProtoCompiler() throws { + // Either set a PROTOC_PATH environment variable to the location of the protoc binary, + // or place the protoc binary in /bin/ + // + // You can run the following from the : + // + // wget https://github.com/protocolbuffers/protobuf/releases/download/v3.6.1/protoc-3.6.1-osx-x86_64.zip + // unzip protoc-3.6.1-osx-x86_64.zip + // + XCTAssertTrue(FileManager.default.fileExists(atPath: environment.protocPath)) + } + + func testProtoCompilerPipeline() throws { + // Given + let data = try compileProto(definition: """ + message int_value { + int32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: 1 + """) + + // Then + XCTAssertEqual([UInt8](data), [0x08, 0x01]) + } + + // MARK: int32 + + func testInt320Decoding() throws { + // Given + let data = try compileProto(definition: """ + message int_value { + int32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: 0 + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 0) + } catch let error { + XCTFail(String(describing: error)) + } + } + + func testInt32PositiveValueDecoding() throws { + // Given + let valuesToTest: [Int32] = [ + 1, 127, // 1 byte range + 128, 16383, // 2 byte range + 16384, 2097151, // 3 byte range + 2097152, 268435455, // 4 byte range + 268435456, Int32.max, // 5 byte range + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message int_value { + int32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: \(value) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + XCTAssertEqual(message.value, .varint(rawValue: UInt64(value))) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + func testInt32OverflowFailsToCompile() throws { + // Given + let value = UInt32.max + + XCTAssertThrowsError(try compileProto(definition: """ + message int_value { + int32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: \(value) + """), "Failed to compile") { error in + XCTAssertTrue(error is ProtoCompilerError) + } + } + + func testInt32NegativeValueDecoding() throws { + // Given + let valuesToTest: [Int32] = [ + -1, -127, + -128, -16383, + -16384, -2097151, + -2097152, -268435455, + -268435456, Int32.min, + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message int_value { + int32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: \(value) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + XCTAssertEqual(message.value, .varint(rawValue: UInt64(bitPattern: Int64(value)))) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + func testMultipleInt32Decoding() throws { + // Given + let data = try compileProto(definition: """ + message int_value { + int32 first_value = 1; + int32 second_value = 2; + int32 third_value = 3; + } + """, message: "int_value", content: """ + first_value: 1 + second_value: 128 + third_value: 268435456 + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages, [ + ProtoMessage(fieldNumber: 1, value: .varint(rawValue: 1)), + ProtoMessage(fieldNumber: 2, value: .varint(rawValue: 128)), + ProtoMessage(fieldNumber: 3, value: .varint(rawValue: 268435456)), + ]) + XCTAssertEqual(messages.count, 3) + } catch let error { + XCTFail(String(describing: error)) + } + } + + // MARK: int64 + + func testInt640Decoding() throws { + // Given + do { + let data = try compileProto(definition: """ + message int_value { + int64 int_value = 1; + } + """, message: "int_value", content: """ + int_value: 0 + """) + let decoder = BinaryDataDecoder() + + // When + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 0) + } catch let error { + XCTFail(String(describing: error)) + } + } + + func testInt64PositiveValueDecoding() throws { + // Given + let valuesToTest: [Int64] = [ + 1, 127, // 1 byte range + 128, 16383, // 2 byte range + 16384, 2097151, // 3 byte range + 2097152, 268435455, // 4 byte range + 268435456, 34359738367, // 5 byte range + 34359738368, 4398046511103, // 6 byte range + 4398046511104, 562949953421311, // 7 byte range + 562949953421312, 72057594037927935, // 8 byte range + 72057594037927936, Int64.max, // 9 byte range + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message int_value { + int64 int_value = 1; + } + """, message: "int_value", content: """ + int_value: \(value) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + XCTAssertEqual(message.value, .varint(rawValue: UInt64(value))) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + func testInt64NegativeValueDecoding() throws { + // Given + let valuesToTest: [Int64] = [ + -1, -127, + -128, -16383, + -16384, -2097151, + -2097152, -268435455, + -268435456, -34359738367, + -34359738368, -4398046511103, + -4398046511104, -562949953421311, + -562949953421312, -72057594037927935, + -72057594037927936, Int64.min + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message int_value { + int64 int_value = 1; + } + """, message: "int_value", content: """ + int_value: \(value) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + XCTAssertEqual(message.value, .varint(rawValue: UInt64(bitPattern: Int64(value)))) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + // MARK: sint32 + + func testSInt320Decoding() throws { + // Given + do { + let data = try compileProto(definition: """ + message int_value { + sint32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: 0 + """) + let decoder = BinaryDataDecoder() + + // When + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 0) + } catch let error { + XCTFail(String(describing: error)) + } + } + + func testSInt32PositiveValueDecoding() throws { + // Given + let valuesToTest: [Int32] = [ + 1, 127, // 1 byte range + 128, 16383, // 2 byte range + 16384, 2097151, // 3 byte range + 2097152, 268435455, // 4 byte range + 268435456, Int32.max, // 5 byte range + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message int_value { + sint32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: \(value) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + // sint values will be zig-zag encoded. + // https://developers.google.com/protocol-buffers/docs/encoding#signed-integers + XCTAssertEqual(message.value, .varint(rawValue: UInt64(UInt32(bitPattern: (value << 1) ^ (value >> 31))))) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + func testSInt32NegativeValueDecoding() throws { + // Given + let valuesToTest: [Int32] = [ + -1, -127, + -128, -16383, + -16384, -2097151, + -2097152, -268435455, + -268435456, Int32.min, + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message int_value { + sint32 int_value = 1; + } + """, message: "int_value", content: """ + int_value: \(value) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + // sint values will be zig-zag encoded. + // https://developers.google.com/protocol-buffers/docs/encoding#signed-integers + XCTAssertEqual(message.value, .varint(rawValue: UInt64(UInt32(bitPattern: (value << 1) ^ (value >> 31))))) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + // MARK: fixed32 + + func testFixed320Decoding() throws { + // Given + do { + let data = try compileProto(definition: """ + message value { + fixed32 value = 1; + } + """, message: "value", content: """ + value: 0 + """) + let decoder = BinaryDataDecoder() + + // When + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 0) + } catch let error { + XCTFail(String(describing: error)) + } + } + + func testFixed32ValueDecoding() throws { + // Given + let valuesToTest: [UInt32] = [ + 1, UInt32.max + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message value { + fixed32 value = 1; + } + """, message: "value", content: """ + value: \(value) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + XCTAssertEqual(message.value, .fixed32(rawValue: value)) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + // MARK: float + + func testFloat0Decoding() throws { + // Given + do { + let data = try compileProto(definition: """ + message float_value { + float float_value = 1; + } + """, message: "float_value", content: """ + float_value: 0 + """) + let decoder = BinaryDataDecoder() + + // When + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 0) + } catch let error { + XCTFail(String(describing: error)) + } + } + + func testFloatValueDecoding() throws { + // Given + let valuesToTest: [Float] = [ + -Float.greatestFiniteMagnitude, -3.14159, -1, 1, 3.14159, Float.greatestFiniteMagnitude, + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message float_value { + float float_value = 1; + } + """, message: "float_value", content: """ + float_value: \(String(format: "%0.20f", value)) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + + XCTAssertEqual(message.value, .fixed32(rawValue: value.bitPattern)) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + // MARK: double + + func testDouble0Decoding() throws { + // Given + do { + let data = try compileProto(definition: """ + message value { + double value = 1; + } + """, message: "value", content: """ + value: 0 + """) + let decoder = BinaryDataDecoder() + + // When + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 0) + } catch let error { + XCTFail(String(describing: error)) + } + } + + func testDoubleValueDecoding() throws { + // Given + let valuesToTest: [Double] = [ + -Double.greatestFiniteMagnitude, -3.14159, -1, 1, 3.14159, Double.greatestFiniteMagnitude, + ] + + for value in valuesToTest { + let data = try compileProto(definition: """ + message value { + double value = 1; + } + """, message: "value", content: """ + value: \(String(format: "%0.20f", value)) + """) + let decoder = BinaryDataDecoder() + + // When + do { + let messages = try decoder.decode([ProtoMessage].self, from: data) + + // Then + XCTAssertEqual(messages.count, 1) + guard let message = messages.first else { + continue + } + XCTAssertEqual(message.fieldNumber, 1) + XCTAssertEqual(message.value, .fixed64(rawValue: value.bitPattern)) + } catch let error { + XCTFail("Value \(value): \(String(describing: error))") + } + } + } + + // MARK: Generated messages + + func testGeneratedMessageDecoding() throws { + // Given + do { + let data = try compileProto(definition: """ + message embedded { + int32 int32_value = 1; + } + message value { + double double_value = 1; + float float_value = 2; + int32 int32_value = 3; + int64 int64_value = 4; + uint32 uint32_value = 5; + uint64 uint64_value = 6; + sint32 sint32_value = 7; + sint32 sint64_value = 8; + fixed32 fixed32_value = 9; + fixed64 fixed64_value = 10; + bool bool_value = 13; + string string_value = 14; + bytes bytes_value = 15; + embedded embedded_value = 16; + int32 missing_value = 20; + } + """, message: "value", content: """ + double_value: 1.34159 + float_value: 1.5234 + int32_value: 1 + int64_value: \(Int64.max) + uint32_value: \(UInt32.max) + uint64_value: \(UInt64.max) + sint32_value: 268435456 + sint64_value: 268435456 + fixed32_value: \(UInt32.max) + fixed64_value: \(UInt64.max) + bool_value: true + string_value: "Some string" + bytes_value: "\\000\\001\\002" + embedded_value { + int32_value: 5678 + } + """) + let decoder = ProtoDecoder() + + // When + let message = try decoder.decode(Message.self, from: data) + + // Then + XCTAssertEqual(message.doubleValue, 1.34159) + XCTAssertEqual(message.floatValue, 1.5234) + XCTAssertEqual(message.int32Value, 1) + XCTAssertEqual(message.int64Value, Int64.max) + XCTAssertEqual(message.uint32Value, UInt32.max) + XCTAssertEqual(message.uint64Value, UInt64.max) + XCTAssertEqual(message.sint32Value, 268435456) + XCTAssertEqual(message.sint64Value, 268435456) + XCTAssertEqual(message.fixed32Value, UInt32.max) + XCTAssertEqual(message.fixed64Value, UInt64.max) + XCTAssertNotNil(message.boolValue) + XCTAssertEqual(message.stringValue, "Some string") + XCTAssertNotNil(message.bytesValue) + if let bytesValue = message.bytesValue { + XCTAssertEqual([UInt8](bytesValue), [0, 1, 2]) + } + if let boolValue = message.boolValue { + XCTAssertTrue(boolValue) + } + XCTAssertNil(message.missingValue) + XCTAssertEqual(message.embedded?.int32Value, 5678) + } catch let error { + XCTFail(String(describing: error)) + } + } + + private func compileProto(definition: String, message: String, content: String) throws -> Data { + let input = temporaryFile() + let proto = temporaryFile() + let output = temporaryFile() + let errors = temporaryFile() + + let package = "\(type(of: self))" + let header = """ + syntax = "proto3"; + + package \(package); + + """ + try (header + definition).write(to: proto, atomically: true, encoding: .utf8) + try content.write(to: input, atomically: true, encoding: .utf8) + + let task = Process() + task.launchPath = environment.protocPath + task.standardInput = try FileHandle(forReadingFrom: input) + task.standardOutput = try FileHandle(forWritingTo: output) + task.standardError = try FileHandle(forWritingTo: errors) + task.arguments = [ + "--encode", + "\(package).\(message)", + "-I", + proto.deletingLastPathComponent().absoluteString.replacingOccurrences(of: "file://", with: ""), + proto.absoluteString.replacingOccurrences(of: "file://", with: "") + ] + task.launch() + task.waitUntilExit() + + let errorText = try String(contentsOf: errors) + if !errorText.isEmpty { + throw ProtoCompilerError.producedErrorOutput(stderr: errorText) + } + + return try Data(contentsOf: output) + } +} + +private enum ProtoCompilerError: Error { + case producedErrorOutput(stderr: String) +} + +private func temporaryFile() -> URL { + let template = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("file.XXXXXX") as NSURL + var buffer = [Int8](repeating: 0, count: Int(PATH_MAX)) + template.getFileSystemRepresentation(&buffer, maxLength: buffer.count) + let fd = mkstemp(&buffer) + guard fd != -1 else { + preconditionFailure("Unable to create temporary file.") + } + return URL(fileURLWithFileSystemRepresentation: buffer, isDirectory: false, relativeTo: nil) +} + +private struct TestConfig { + var testAgainstProtoc = true + var protocPath: String = { + + return URL(fileURLWithPath: #file) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("bin") + .appendingPathComponent("protoc") + .absoluteString + .replacingOccurrences(of: "file://", with: "") + }() + + static var environment: TestConfig { + var config = TestConfig() + if let protocPath = getEnvironmentVariable(named: "PROTOC_PATH") { + config.protocPath = protocPath + } + return config + } +} + +private func getEnvironmentVariable(named name: String) -> String? { + if let environmentValue = getenv(name) { + return String(cString: environmentValue) + } else { + return nil + } +}