Skip to content

Commit

Permalink
Add binaryStreamDecodingError code
Browse files Browse the repository at this point in the history
  • Loading branch information
gjcairo committed Apr 10, 2024
1 parent 1fae089 commit 1405d18
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 82 deletions.
6 changes: 3 additions & 3 deletions Sources/SwiftProtobuf/AsyncMessageSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public struct AsyncMessageSequence<
shift += UInt64(7)
if shift > 35 {
iterator = nil
throw SwiftProtobufError.BinaryDecoding.malformedLength()
throw SwiftProtobufError.BinaryStreamDecoding.malformedLength()
}
if (byte & 0x80 == 0) {
return messageSize
Expand All @@ -131,7 +131,7 @@ public struct AsyncMessageSequence<
if (shift > 0) {
// The stream has ended inside a varint.
iterator = nil
throw SwiftProtobufError.BinaryDecoding.truncated()
throw SwiftProtobufError.BinaryStreamDecoding.truncated()
}
return nil // End of stream reached.
}
Expand All @@ -153,7 +153,7 @@ public struct AsyncMessageSequence<
guard let byte = try await iterator?.next() else {
// The iterator hit the end, but the chunk wasn't filled, so the full
// payload wasn't read.
throw SwiftProtobufError.BinaryDecoding.truncated()
throw SwiftProtobufError.BinaryStreamDecoding.truncated()
}
chunk[consumedBytes] = byte
consumedBytes += 1
Expand Down
81 changes: 12 additions & 69 deletions Sources/SwiftProtobuf/BinaryDelimited.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,63 +16,6 @@
import Foundation
#endif

extension SwiftProtobufError.BinaryDecoding {
/// If a read/write to the stream fails, but the stream's `streamError` is nil,
/// this error will be thrown instead since the stream didn't provide anything
/// more specific. A common cause for this can be failing to open the stream
/// before trying to read/write to it.
public static func unknownStreamError(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryDecodingError,
message: "Unknown error when reading/writing binary-delimited message into stream.",
location: .init(function: function, file: file, line: line)
)
}

/// While attempting to read the length of a message on the stream, the
/// bytes were malformed for the protobuf format.
public static func malformedLength(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryDecodingError,
message: """
While attempting to read the length of a binary-delimited message \
on the stream, the bytes were malformed for the protobuf format.
""",
location: .init(function: function, file: file, line: line)
)
}

/// This isn't really an error. `InputStream` documents that
/// `hasBytesAvailable` _may_ return `True` if a read is needed to
/// determine if there really are bytes available. So this "error" is thrown
/// when a `parse` or `merge` fails because there were no bytes available.
/// If this is raised, the callers should decide via what ever other means
/// are correct if the stream has completely ended or if more bytes might
/// eventually show up.
public static func noBytesAvailable(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryDecodingError,
message: """
This is not really an error: please read the documentation for
`SwiftProtobufError/BinaryDecoding/noBytesAvailable` for more information.
""",
location: .init(function: function, file: file, line: line)
)
}
}

/// Helper methods for reading/writing messages with a length prefix.
public enum BinaryDelimited {
/// Additional errors for delimited message handing.
Expand Down Expand Up @@ -103,7 +46,7 @@ public enum BinaryDelimited {
/// - partial: If `false` (the default), this method will check
/// ``Message/isInitialized-6abgi`` before encoding to verify that all required
/// fields are present. If any are missing, this method throws
/// ``SwiftProtobufError/BinaryEncoding/missingRequiredFields``.
/// ``SwiftProtobufError/BinaryStreamDecoding/missingRequiredFields``.
/// - Throws: ``SwiftProtobufError`` if encoding fails or some writing errors occur; or the
/// underlying `OutputStream.streamError` for a stream error.
public static func serialize(
Expand Down Expand Up @@ -136,9 +79,9 @@ public enum BinaryDelimited {
if let streamError = stream.streamError {
throw streamError
}
throw SwiftProtobufError.BinaryDecoding.unknownStreamError()
throw SwiftProtobufError.BinaryStreamDecoding.unknownStreamError()
}
throw SwiftProtobufError.BinaryEncoding.truncated()
throw SwiftProtobufError.BinaryStreamDecoding.truncated()
}
}

Expand All @@ -158,7 +101,7 @@ public enum BinaryDelimited {
/// - partial: If `false` (the default), this method will check
/// ``Message/isInitialized-6abgi`` after decoding to verify that all required
/// fields are present. If any are missing, this method throws
/// ``SwiftProtobufError/BinaryDecoding/missingRequiredFields``.
/// ``SwiftProtobufError/BinaryStreamDecoding/missingRequiredFields``.
/// - options: The ``BinaryDecodingOptions`` to use.
/// - Returns: The message read.
/// - Throws: ``SwiftProtobufError`` if decoding fails, and for some reading errors; or the
Expand Down Expand Up @@ -199,7 +142,7 @@ public enum BinaryDelimited {
/// - partial: If `false` (the default), this method will check
/// ``Message/isInitialized-6abgi`` after decoding to verify that all required
/// fields are present. If any are missing, this method throws
/// ``SwiftProtobufError/BinaryDecoding/missingRequiredFields``.
/// ``SwiftProtobufError/BinaryStreamDecoding/missingRequiredFields``.
/// - options: The BinaryDecodingOptions to use.
/// - Throws: ``SwiftProtobufError`` if decoding fails, and for some reading errors; or the
/// underlying `InputStream.streamError` for a stream error.
Expand All @@ -216,7 +159,7 @@ public enum BinaryDelimited {
return
}
guard unsignedLength <= 0x7fffffff else {
throw SwiftProtobufError.BinaryDecoding.tooLarge()
throw SwiftProtobufError.BinaryStreamDecoding.tooLarge()
}
let length = Int(unsignedLength)

Expand All @@ -243,11 +186,11 @@ public enum BinaryDelimited {
if let streamError = stream.streamError {
throw streamError
}
throw SwiftProtobufError.BinaryDecoding.unknownStreamError()
throw SwiftProtobufError.BinaryStreamDecoding.unknownStreamError()
}
if bytesRead == 0 {
// Hit the end of the stream
throw SwiftProtobufError.BinaryDecoding.truncated()
throw SwiftProtobufError.BinaryStreamDecoding.truncated()
}
if bytesRead < chunk.count {
data += chunk[0..<bytesRead]
Expand Down Expand Up @@ -287,7 +230,7 @@ internal func decodeVarint(_ stream: InputStream) throws -> UInt64 {
if let streamError = stream.streamError {
throw streamError
}
throw SwiftProtobufError.BinaryDecoding.unknownStreamError()
throw SwiftProtobufError.BinaryStreamDecoding.unknownStreamError()
}
}

Expand All @@ -296,17 +239,17 @@ internal func decodeVarint(_ stream: InputStream) throws -> UInt64 {
while true {
guard let c = try nextByte() else {
if shift == 0 {
throw SwiftProtobufError.BinaryDecoding.noBytesAvailable()
throw SwiftProtobufError.BinaryStreamDecoding.noBytesAvailable()
}
throw SwiftProtobufError.BinaryDecoding.truncated()
throw SwiftProtobufError.BinaryStreamDecoding.truncated()
}
value |= UInt64(c & 0x7f) << shift
if c & 0x80 == 0 {
return value
}
shift += 7
if shift > 63 {
throw SwiftProtobufError.BinaryDecoding.malformedLength()
throw SwiftProtobufError.BinaryStreamDecoding.malformedLength()
}
}
}
Expand Down
92 changes: 92 additions & 0 deletions Sources/SwiftProtobuf/SwiftProtobufError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ extension SwiftProtobufError {
private enum Wrapped: Hashable, Sendable, CustomStringConvertible {
case binaryEncodingError
case binaryDecodingError
case binaryStreamDecodingError
case jsonEncodingError
case jsonDecodingError
case textFormatDecodingError
Expand All @@ -103,6 +104,8 @@ extension SwiftProtobufError {
return "Binary encoding error"
case .binaryDecodingError:
return "Binary decoding error"
case .binaryStreamDecodingError:
return "Stream decoding error"
case .jsonEncodingError:
return "JSON encoding error"
case .jsonDecodingError:
Expand Down Expand Up @@ -134,6 +137,10 @@ extension SwiftProtobufError {
Self(.binaryDecodingError)
}

public static var binaryStreamDecodingError: Self {
Self(.binaryStreamDecodingError)
}

public static var jsonEncodingError: Self {
Self(.jsonEncodingError)
}
Expand Down Expand Up @@ -390,6 +397,91 @@ extension SwiftProtobufError {
}
}

/// Errors arising from decoding streams of binary messages. These errors have to do with the framing
/// of the messages in the stream, or the stream as a whole.
public enum BinaryStreamDecoding {
/// If a read/write to the stream fails, but the stream's `streamError` is nil,
/// this error will be thrown instead since the stream didn't provide anything
/// more specific. A common cause for this can be failing to open the stream
/// before trying to read/write to it.
public static func unknownStreamError(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryStreamDecodingError,
message: "Unknown error when reading/writing binary-delimited message into stream.",
location: .init(function: function, file: file, line: line)
)
}

/// While reading/writing to the stream, less than the expected bytes was read/written.
public static func truncated(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryStreamDecodingError,
message: "The end of the data was reached before it was expected.",
location: SourceLocation(function: function, file: file, line: line)
)
}

/// Message is too large. Bytes and Strings have a max size of 2GB.
public static func tooLarge(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryStreamDecodingError,
message: "Message too large: Bytes and Strings have a max size of 2GB.",
location: SourceLocation(function: function, file: file, line: line)
)
}

/// While attempting to read the length of a message on the stream, the
/// bytes were malformed for the protobuf format.
public static func malformedLength(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryStreamDecodingError,
message: """
While attempting to read the length of a binary-delimited message \
on the stream, the bytes were malformed for the protobuf format.
""",
location: .init(function: function, file: file, line: line)
)
}

/// This isn't really an error. `InputStream` documents that
/// `hasBytesAvailable` _may_ return `True` if a read is needed to
/// determine if there really are bytes available. So this "error" is thrown
/// when a `parse` or `merge` fails because there were no bytes available.
/// If this is raised, the callers should decide via what ever other means
/// are correct if the stream has completely ended or if more bytes might
/// eventually show up.
public static func noBytesAvailable(
function: String = #function,
file: String = #fileID,
line: Int = #line
) -> SwiftProtobufError {
SwiftProtobufError(
code: .binaryStreamDecodingError,
message: """
This is not really an error: please read the documentation for
`SwiftProtobufError/BinaryStreamDecoding/noBytesAvailable` for more information.
""",
location: .init(function: function, file: file, line: line)
)
}
}

/// Errors arising from encoding protobufs into JSON.
public enum JSONEncoding {
/// Timestamp values can only be JSON encoded if they hold a value
Expand Down
12 changes: 6 additions & 6 deletions Tests/SwiftProtobufTests/Test_AsyncMessageSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ final class Test_AsyncMessageSequence: XCTestCase {
XCTFail("Shouldn't have returned a value for an empty stream.")
}
} catch {
if self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryDecoding.truncated()) {
if self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryStreamDecoding.truncated()) {
truncatedThrown = true
}
}
XCTAssertTrue(truncatedThrown, "Should throw a SwiftProtobufError.BinaryDecoding.truncated")
XCTAssertTrue(truncatedThrown, "Should throw a SwiftProtobufError.BinaryStreamDecoding.truncated")
}

// Single varint describing a 2GB message
Expand All @@ -157,11 +157,11 @@ final class Test_AsyncMessageSequence: XCTestCase {
XCTFail("Shouldn't have returned a value for an empty stream.")
}
} catch {
if self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryDecoding.truncated()) {
if self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryStreamDecoding.truncated()) {
truncatedThrown = true
}
}
XCTAssertTrue(truncatedThrown, "Should throw a BinaryDelimited.Error.truncated")
XCTAssertTrue(truncatedThrown, "Should throw a SwiftProtobufError.BinaryStreamDecoding.truncated")
}

// Stream with a valid varint and message, but the following varint is truncated
Expand Down Expand Up @@ -189,11 +189,11 @@ final class Test_AsyncMessageSequence: XCTestCase {
}
XCTAssertEqual(count, 1, "One message should be deserialized")
} catch {
if self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryDecoding.truncated()) {
if self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryStreamDecoding.truncated()) {
truncatedThrown = true
}
}
XCTAssertTrue(truncatedThrown, "Should throw a BinaryDelimited.Error.truncated")
XCTAssertTrue(truncatedThrown, "Should throw a SwiftProtobuf.BinaryStreamDecoding.truncated")
}

// Slow test case found by oss-fuzz: 1 million zero-sized messages
Expand Down
8 changes: 4 additions & 4 deletions Tests/SwiftProtobufTests/Test_BinaryDelimited.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ final class Test_BinaryDelimited: XCTestCase {
func assertParseFails(atEndOfStream istream: InputStream) {
XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryDecoding.noBytesAvailable()))
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryStreamDecoding.noBytesAvailable()))
}
}

func assertParsing(failsWithTruncatedStream istream: InputStream) {
XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryDecoding.truncated()))
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryStreamDecoding.truncated()))
}
}

Expand Down Expand Up @@ -90,7 +90,7 @@ final class Test_BinaryDelimited: XCTestCase {

XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryDecoding.tooLarge()))
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryStreamDecoding.tooLarge()))
}
}

Expand All @@ -99,7 +99,7 @@ final class Test_BinaryDelimited: XCTestCase {

XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryDecoding.malformedLength()))
XCTAssertTrue(self.isSwiftProtobufErrorEqual(error as! SwiftProtobufError, .BinaryStreamDecoding.malformedLength()))
}
}

Expand Down

0 comments on commit 1405d18

Please sign in to comment.