Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add two new unique delimited error code for better caller handling #1449

Merged
merged 3 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions Sources/SwiftProtobuf/AsyncMessageSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,9 @@ extension AsyncSequence where Element == UInt8 {
/// `BinaryDecodingError.missingRequiredFields` will be thrown.
/// - options: The BinaryDecodingOptions to use.
/// - Returns: An asynchronous sequence of messages read from the `AsyncSequence` of bytes.
/// - Throws: `BinaryDecodingError` if decoding fails, throws
/// `BinaryDelimited.Error` for some reading errors,
/// `BinaryDecodingError.truncated` if the stream ends before fully decoding a
/// message or a delimiter,
/// `BinaryDecodingError.malformedProtobuf`if a delimiter could not be read and
/// `BinaryDecodingError.tooLarge` if a size delimiter of 2GB or greater is found.
/// - Throws: `BinaryDelimited.Error` for errors in the framing of the messages
/// in the sequence, `BinaryDecodingError` for errors while decoding
/// messages.
@inlinable
public func binaryProtobufDelimitedMessages<M: Message>(
of messageType: M.Type = M.self,
Expand Down Expand Up @@ -78,12 +75,9 @@ public struct AsyncMessageSequence<
/// `BinaryDecodingError.missingRequiredFields` will be thrown.
/// - options: The BinaryDecodingOptions to use.
/// - Returns: An asynchronous sequence of messages read from the `AsyncSequence` of bytes.
/// - Throws: `BinaryDecodingError` if decoding fails, throws
/// `BinaryDelimited.Error` for some reading errors,
/// `BinaryDecodingError.truncated` if the stream ends before fully decoding a
/// message or a delimiter,
/// `BinaryDecodingError.malformedProtobuf`if a delimiter could not be read and
/// `BinaryDecodingError.tooLarge` if a size delimiter of 2GB or greater is found.
/// - Throws: `BinaryDelimited.Error` for errors in the framing of the messages
/// in the sequence, `BinaryDecodingError` for errors while decoding
/// messages.
public init(
base: Base,
extensions: ExtensionMap? = nil,
Expand Down Expand Up @@ -130,7 +124,7 @@ public struct AsyncMessageSequence<
shift += UInt64(7)
if shift > 35 {
iterator = nil
throw BinaryDecodingError.malformedProtobuf
throw BinaryDelimited.Error.malformedLength
}
if (byte & 0x80 == 0) {
return messageSize
Expand All @@ -139,7 +133,7 @@ public struct AsyncMessageSequence<
if (shift > 0) {
// The stream has ended inside a varint.
iterator = nil
throw BinaryDecodingError.truncated
throw BinaryDelimited.Error.truncated
}
return nil // End of stream reached.
}
Expand All @@ -161,7 +155,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 BinaryDecodingError.truncated
throw BinaryDelimited.Error.truncated
}
chunk[consumedBytes] = byte
consumedBytes += 1
Expand Down
46 changes: 33 additions & 13 deletions Sources/SwiftProtobuf/BinaryDelimited.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ public enum BinaryDelimited {
/// Messages are limited by the protobuf spec to 2GB; when decoding, if the
/// length says the payload is over 2GB, this error is raised.
case tooLarge

/// While attempting to read the length of a message on the stream, the
/// bytes were malformed for the protobuf format.
case malformedLength
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change this enum to be a struct on main now. Furthermore, we should double check if there are any other public enums that we might want to convert. This is due to the fact that adding enum cases is a breaking change and is going to limit us down the line. Lately, we have been picking this error pattern: https://github.com/swift-server/swift-memcache-gsoc/blob/main/Sources/SwiftMemcache/MemcachedError.swift

This has a bunch of useful information and let's us extend the codes without breaking API.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've had revisiting the errors on the list for a while #1223, but this small fix to get better error codes in this one case is all the time I've got at the moment.

If someone else has the type to go look at converting over the others that would be great.


/// 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 throw
/// when a `parse` or `merge` fails because there were no bytes available.
/// If this is rasied, the callers should decides via what ever other means
/// are correct if the stream has completely ended or if more bytes might
/// eventually show up.
case noBytesAvailable
}

/// Serialize a single size-delimited message to the given stream. Delimited
Expand Down Expand Up @@ -110,7 +123,7 @@ public enum BinaryDelimited {
/// - Returns: The message read.
/// - Throws: `BinaryDecodingError` if decoding fails, throws
/// `BinaryDelimited.Error` for some reading errors, and the
/// underlying InputStream.streamError for a stream error.
/// underlying `InputStream.streamError` for a stream error.
public static func parse<M: Message>(
messageType: M.Type,
from stream: InputStream,
Expand Down Expand Up @@ -151,7 +164,7 @@ public enum BinaryDelimited {
/// - options: The BinaryDecodingOptions to use.
/// - Throws: `BinaryDecodingError` if decoding fails, throws
/// `BinaryDelimited.Error` for some reading errors, and the
/// underlying InputStream.streamError for a stream error.
/// underlying `InputStream.streamError` for a stream error.
public static func merge<M: Message>(
into message: inout M,
from stream: InputStream,
Expand Down Expand Up @@ -222,31 +235,38 @@ internal func decodeVarint(_ stream: InputStream) throws -> UInt64 {
let readBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: 1)
defer { readBuffer.deallocate() }

func nextByte() throws -> UInt8 {
func nextByte() throws -> UInt8? {
let bytesRead = stream.read(readBuffer, maxLength: 1)
if bytesRead != 1 {
if bytesRead == -1 {
if let streamError = stream.streamError {
throw streamError
}
throw BinaryDelimited.Error.unknownStreamError
switch bytesRead {
case 1:
return readBuffer[0]
case 0:
return nil
default:
precondition(bytesRead == -1)
if let streamError = stream.streamError {
throw streamError
}
throw BinaryDelimited.Error.truncated
throw BinaryDelimited.Error.unknownStreamError
}
return readBuffer[0]
}

var value: UInt64 = 0
var shift: UInt64 = 0
while true {
let c = try nextByte()
guard let c = try nextByte() else {
if shift == 0 {
throw BinaryDelimited.Error.noBytesAvailable
}
throw BinaryDelimited.Error.truncated
}
value |= UInt64(c & 0x7f) << shift
if c & 0x80 == 0 {
return value
}
shift += 7
if shift > 63 {
throw BinaryDecodingError.malformedProtobuf
throw BinaryDelimited.Error.malformedLength
}
}
}
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 error as! BinaryDecodingError == .truncated {
if error as! BinaryDelimited.Error == .truncated {
truncatedThrown = true
}
}
XCTAssertTrue(truncatedThrown, "Should throw a BinaryDecodingError.truncated")
XCTAssertTrue(truncatedThrown, "Should throw a BinaryDelimited.Error.truncated")
}

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

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

fileprivate func asyncByteStream(bytes: [UInt8]) -> AsyncStream<UInt8> {
Expand Down
112 changes: 93 additions & 19 deletions Tests/SwiftProtobufTests/Test_BinaryDelimited.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,98 @@ import Foundation
import XCTest
import SwiftProtobuf

fileprivate func openInputStream(_ bytes: [UInt8]) -> InputStream {
let istream = InputStream(data: Data(bytes))
istream.open()
return istream
}

class Test_BinaryDelimited: XCTestCase {

func testEverything() {
// Don't need to test encode/decode since there are plenty of tests specific to that,
// just test the delimited behaviors.
/// Helper to assert the next message read matches and expected one.
func assertParse<M: Message & Equatable>(expected: M, onStream istream: InputStream) {
do {
let msg = try BinaryDelimited.parse(
messageType: M.self,
from: istream)
XCTAssertEqual(msg, expected)
} catch let e {
XCTFail("Unexpected failure: \(e)")
}
}

/// Helper to assert we're at the end of the stream.
///
/// `hasBytesAvailable` is documented as maybe returning True and a read
/// has to happen to really know if ones at the end. This is especially
/// true with file based streams.
func assertParseFails(atEndOfStream istream: InputStream) {
XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertEqual(error as? BinaryDelimited.Error, BinaryDelimited.Error.noBytesAvailable)
}
}

func assertParsing(failsWithTruncatedStream istream: InputStream) {
XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertEqual(error as? BinaryDelimited.Error, BinaryDelimited.Error.truncated)
}
}

func testNoData() {
let istream = openInputStream([])

assertParseFails(atEndOfStream: istream)
}

func testZeroLengthMessage() {
let istream = openInputStream([0])

assertParse(expected: SwiftProtoTesting_TestAllTypes(), onStream: istream)

assertParseFails(atEndOfStream: istream)
}

func testNoDataForMessage() {
let istream = openInputStream([0x96, 0x01])

// Length will be read, then the no data for the message, so .truncated.
assertParsing(failsWithTruncatedStream: istream)
}

func testNotEnoughDataForMessage() {
let istream = openInputStream([0x96, 0x01, 0x01, 0x02, 0x03])

// Length will be read, but not enought data, so .truncated
assertParsing(failsWithTruncatedStream: istream)
}

func testTruncatedLength() {
let istream = openInputStream([0x96]) // Needs something like `, 0x01`

assertParsing(failsWithTruncatedStream: istream)
}

func testTooLarge() {
let istream = openInputStream([0x80, 0x80, 0x80, 0x80, 0x08]) // 2GB

XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertEqual(error as? BinaryDelimited.Error, BinaryDelimited.Error.tooLarge)
}
}

func testOverEncodedLength() {
let istream = openInputStream([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 ,0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x08])

XCTAssertThrowsError(try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self,
from: istream)) { error in
XCTAssertEqual(error as? BinaryDelimited.Error, BinaryDelimited.Error.malformedLength)
}
}

func testTwoMessages() {
let stream1 = OutputStream.toMemory()
stream1.open()

Expand Down Expand Up @@ -48,27 +134,15 @@ class Test_BinaryDelimited: XCTestCase {
let stream2 = InputStream(data: data)
stream2.open()

// Test using `merge`
var msg1a = SwiftProtoTesting_TestAllTypes()
XCTAssertNoThrow(try BinaryDelimited.merge(into: &msg1a, from: stream2))
XCTAssertEqual(msg1, msg1a)

do {
let msg2a = try BinaryDelimited.parse(
messageType: SwiftProtoTesting_TestPackedTypes.self,
from: stream2)
XCTAssertEqual(msg2, msg2a)
} catch let e {
XCTFail("Unexpected failure: \(e)")
}
// Test using `parse`
assertParse(expected: msg2, onStream: stream2)

do {
_ = try BinaryDelimited.parse(messageType: SwiftProtoTesting_TestAllTypes.self, from: stream2)
XCTFail("Should not have gotten here")
} catch BinaryDelimited.Error.truncated {
// Nothing, this is what we expect since there is nothing left to read.
} catch let e {
XCTFail("Unexpected failure: \(e)")
}
assertParseFails(atEndOfStream: stream2)
}

}