Repository: apple/swift-nio-extras
Pull request #9 (2018-07-14 08:47:04)

FixedLengthFrameDecoder
Implement a decoder for frames with a fixed length.  https://github.com/apple/swift-nio/pull/479

Motivation:
Sometimes messages come in fixed length frames. https://github.com/apple/swift-nio/issues/474

Modifications:
Added a FixedLengthFrameDecoder.

Result:
A decoder for messages with a fixed length.

swift-nio-bot: Can one of the admins verify this patch?
Lukasa: @swift-nio-bot test this please

Patch:
diff --git a/Sources/NIOExtras/FixedLengthDecoder.swift b/Sources/NIOExtras/FixedLengthDecoder.swift
new file mode 100644
index 0000000..be640e8
--- /dev/null
+++ b/Sources/NIOExtras/FixedLengthDecoder.swift
@@ -0,0 +1,61 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import NIO
+///
+/// A decoder that splits the received `ByteBuffer` by a fixed number
+/// of bytes. For example, if you received the following four fragmented packets:
+///
+///     +---+----+------+----+
+///     | A | BC | DEFG | HI |
+///     +---+----+------+----+
+///
+/// A `FixedLengthFrameDecoder` will decode them into the
+/// following three packets with the fixed length:
+///
+///     +-----+-----+-----+
+///     | ABC | DEF | GHI |
+///     +-----+-----+-----+
+///
+public final class FixedLengthFrameDecoder: ByteToMessageDecoder {
+    public typealias InboundIn = ByteBuffer
+    public typealias InboundOut = ByteBuffer
+
+    public var cumulationBuffer: ByteBuffer?
+
+    private let frameLength: Int
+
+    /// Create `FixedLengthFrameDecoder` with a given frame length.
+    ///
+    /// - parameters:
+    ///    - frameLength: The length of a frame.
+    public init(frameLength: Int) {
+        self.frameLength = frameLength
+    }
+
+    public func decode(ctx: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
+        guard let slice = buffer.readSlice(length: frameLength) else {
+            return .needMoreData
+        }
+
+        ctx.fireChannelRead(self.wrapInboundOut(slice))
+        return .continue
+    }
+
+    public func handlerRemoved(ctx: ChannelHandlerContext) {
+        if let buffer = cumulationBuffer, buffer.readableBytes > 0 {
+            ctx.fireErrorCaught(NIOExtrasErrors.LeftOverBytesError(leftOverBytes: buffer))
+        }
+    }
+}
diff --git a/Sources/NIOExtras/NIOExtrasError.swift b/Sources/NIOExtras/NIOExtrasError.swift
new file mode 100644
index 0000000..43007ce
--- /dev/null
+++ b/Sources/NIOExtras/NIOExtrasError.swift
@@ -0,0 +1,25 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+import NIO
+
+public protocol NIOExtrasError: Equatable, Error { }
+
+/// Errors that are raised in NIOExtras.
+public enum NIOExtrasErrors {
+
+    /// Error indicating that after an operation some unused bytes are left.
+    public struct LeftOverBytesError: NIOExtrasError {
+        public let leftOverBytes: ByteBuffer
+    }
+}


Test patch:
diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift
index c8d2ca0..e829f02 100644
--- a/Tests/LinuxMain.swift
+++ b/Tests/LinuxMain.swift
@@ -26,6 +26,7 @@ import XCTest
    @testable import NIOExtrasTests
 
    XCTMain([
+         testCase(FixedLengthFrameDecoderTest.allTests),
          testCase(QuiescingHelperTest.allTests),
     ])
 #endif
diff --git a/Tests/NIOExtrasTests/FixedLengthDecoderTest+XCTest.swift b/Tests/NIOExtrasTests/FixedLengthDecoderTest+XCTest.swift
new file mode 100644
index 0000000..20d119b
--- /dev/null
+++ b/Tests/NIOExtrasTests/FixedLengthDecoderTest+XCTest.swift
@@ -0,0 +1,36 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+//
+// FixedLengthDecoderTest+XCTest.swift
+//
+import XCTest
+
+///
+/// NOTE: This file was generated by generate_linux_tests.rb
+///
+/// Do NOT edit this file directly as it will be regenerated automatically when needed.
+///
+
+extension FixedLengthFrameDecoderTest {
+
+   static var allTests : [(String, (FixedLengthFrameDecoderTest) -> () throws -> Void)] {
+      return [
+                ("testDecodeIfFewerBytesAreSent", testDecodeIfFewerBytesAreSent),
+                ("testDecodeIfMoreBytesAreSent", testDecodeIfMoreBytesAreSent),
+                ("testRemoveHandlerWhenBufferIsNotEmpty", testRemoveHandlerWhenBufferIsNotEmpty),
+                ("testRemoveHandlerWhenBufferIsEmpty", testRemoveHandlerWhenBufferIsEmpty),
+           ]
+   }
+}
+
diff --git a/Tests/NIOExtrasTests/FixedLengthDecoderTest.swift b/Tests/NIOExtrasTests/FixedLengthDecoderTest.swift
new file mode 100644
index 0000000..18ff154
--- /dev/null
+++ b/Tests/NIOExtrasTests/FixedLengthDecoderTest.swift
@@ -0,0 +1,104 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the SwiftNIO open source project
+//
+// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
+//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+import XCTest
+import NIO
+import NIOExtras
+
+class FixedLengthFrameDecoderTest: XCTestCase {
+
+    public func testDecodeIfFewerBytesAreSent() throws {
+        let channel = EmbeddedChannel()
+
+        let frameLength = 8
+        try channel.pipeline.add(handler: FixedLengthFrameDecoder(frameLength: frameLength)).wait()
+
+        var buffer = channel.allocator.buffer(capacity: frameLength)
+        buffer.write(string: "xxxx")
+        XCTAssertFalse(try channel.writeInbound(buffer))
+        XCTAssertTrue(try channel.writeInbound(buffer))
+
+        var outputBuffer: ByteBuffer? = channel.readInbound()
+        XCTAssertEqual("xxxxxxxx", outputBuffer?.readString(length: frameLength))
+        XCTAssertFalse(try channel.finish())
+    }
+
+    public func testDecodeIfMoreBytesAreSent() throws {
+        let channel = EmbeddedChannel()
+
+        let frameLength = 8
+        try channel.pipeline.add(handler: FixedLengthFrameDecoder(frameLength: frameLength)).wait()
+
+        var buffer = channel.allocator.buffer(capacity: 19)
+        buffer.write(string: "xxxxxxxxaaaaaaaabbb")
+        XCTAssertTrue(try channel.writeInbound(buffer))
+
+        var outputBuffer: ByteBuffer? = channel.readInbound()
+        XCTAssertEqual("xxxxxxxx", outputBuffer?.readString(length: frameLength))
+
+        outputBuffer = channel.readInbound()
+        XCTAssertEqual("aaaaaaaa", outputBuffer?.readString(length: frameLength))
+
+        outputBuffer = channel.readInbound()
+        XCTAssertNil(outputBuffer?.readString(length: frameLength))
+        XCTAssertFalse(try channel.finish())
+    }
+
+    public func testRemoveHandlerWhenBufferIsNotEmpty() throws {
+        let channel = EmbeddedChannel()
+
+        let frameLength = 8
+        let handler = FixedLengthFrameDecoder(frameLength: frameLength)
+        try channel.pipeline.add(handler: handler).wait()
+
+        var buffer = channel.allocator.buffer(capacity: 15)
+        buffer.write(string: "xxxxxxxxxxxxxxx")
+        XCTAssertTrue(try channel.writeInbound(buffer))
+
+        var outputBuffer: ByteBuffer? = channel.readInbound()
+        XCTAssertEqual("xxxxxxxx", outputBuffer?.readString(length: frameLength))
+
+        _ = try channel.pipeline.remove(handler: handler).wait()
+        XCTAssertThrowsError(try channel.throwIfErrorCaught()) { error in
+            guard let error = error as? NIOExtrasErrors.LeftOverBytesError else {
+                XCTFail()
+                return
+            }
+
+            var expectedBuffer = channel.allocator.buffer(capacity: 7)
+            expectedBuffer.write(string: "xxxxxxx")
+            XCTAssertEqual(error.leftOverBytes, expectedBuffer)
+        }
+        XCTAssertFalse(try channel.finish())
+    }
+
+    public func testRemoveHandlerWhenBufferIsEmpty() throws {
+        let channel = EmbeddedChannel()
+
+        let frameLength = 8
+        let handler = FixedLengthFrameDecoder(frameLength: frameLength)
+        try channel.pipeline.add(handler: handler).wait()
+
+        var buffer = channel.allocator.buffer(capacity: 6)
+        buffer.write(string: "xxxxxxxx")
+        XCTAssertTrue(try channel.writeInbound(buffer))
+
+        var outputBuffer: ByteBuffer? = channel.readInbound()
+        XCTAssertEqual("xxxxxxxx", outputBuffer?.readString(length: frameLength))
+
+        _ = try channel.pipeline.remove(handler: handler).wait()
+        XCTAssertNoThrow(try channel.throwIfErrorCaught())
+        XCTAssertFalse(try channel.finish())
+    }
+}