Skip to content

Commit 783ca54

Browse files
authored
Created a decoder handler to deserialize a MemcachedResponse (#11)
* created a MemcachedResponse decoder * flags are parsed but not handled * soundness fix * response status name cleanup * eof not needed * accessor not needed * Response and decoder refactor * reimpl decoder as state machine * decoder test clean up * no need to move reader in decodeEndOfLine * ignore flags * consume flag but do nothing with it * revamp of decoder, removed un needed state * remove unused state, added fatal error
1 parent 523321a commit 783ca54

File tree

6 files changed

+351
-10
lines changed

6 files changed

+351
-10
lines changed

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ let package = Package(
3939
dependencies: [
4040
.product(name: "NIOCore", package: "swift-nio"),
4141
.product(name: "NIOPosix", package: "swift-nio"),
42+
.product(name: "NIOEmbedded", package: "swift-nio"),
4243
.product(name: "Logging", package: "swift-log"),
4344
]
4445
),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the swift-memcache-gsoc open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the swift-memcache-gsoc project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of swift-memcache-gsoc project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
struct MemcachedResponse {
16+
enum ReturnCode {
17+
case HD
18+
case NS
19+
case EX
20+
case NF
21+
case VA
22+
23+
init(_ bytes: UInt16) {
24+
switch bytes {
25+
case 0x4844:
26+
self = .HD
27+
case 0x4E53:
28+
self = .NS
29+
case 0x4558:
30+
self = .EX
31+
case 0x4E46:
32+
self = .NF
33+
case 0x5641:
34+
self = .VA
35+
default:
36+
preconditionFailure("Unrecognized response code.")
37+
}
38+
}
39+
}
40+
41+
var returnCode: ReturnCode
42+
var dataLength: UInt64?
43+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the swift-memcache-gsoc open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the swift-memcache-gsoc project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of swift-memcache-gsoc project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOPosix
17+
18+
/// Responses look like:
19+
///
20+
/// <RC> <datalen*> <flag1> <flag2> <...>\r\n
21+
///
22+
/// Where <RC> is a 2 character return code. The number of flags returned are
23+
/// based off of the flags supplied.
24+
///
25+
/// <datalen> is only for responses with payloads, with the return code 'VA'.
26+
///
27+
/// Flags are single character codes, ie 'q' or 'k' or 'I', which adjust the
28+
/// behavior of the command. If a flag requests a response flag (ie 't' for TTL
29+
/// remaining), it is returned in the same order as they were in the original
30+
/// command, though this is not strict.
31+
///
32+
/// Flags are single character codes, ie 'q' or 'k' or 'O', which adjust the
33+
/// behavior of a command. Flags may contain token arguments, which come after the
34+
/// flag and before the next space or newline, ie 'Oopaque' or 'Kuserkey'. Flags
35+
/// can return new data or reflect information, in the same order they were
36+
/// supplied in the request. Sending an 't' flag with a get for an item with 20
37+
/// seconds of TTL remaining, would return 't20' in the response.
38+
///
39+
/// All commands accept a tokens 'P' and 'L' which are completely ignored. The
40+
/// arguments to 'P' and 'L' can be used as hints or path specifications to a
41+
/// proxy or router inbetween a client and a memcached daemon. For example, a
42+
/// client may prepend a "path" in the key itself: "mg /path/foo v" or in a proxy
43+
/// token: "mg foo Lpath/ v" - the proxy may then optionally remove or forward the
44+
/// token to a memcached daemon, which will ignore them.
45+
///
46+
/// Syntax errors are handled the same as noted under 'Error strings' section
47+
/// below.
48+
///
49+
/// For usage examples beyond basic syntax, please see the wiki:
50+
/// https://github.com/memcached/memcached/wiki/MetaCommands
51+
struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
52+
typealias InboundOut = MemcachedResponse
53+
54+
/// Describes the errors that can occur during the decoding process.
55+
enum MemcachedDecoderError: Error {
56+
/// This error is thrown when EOF is encountered but there are still
57+
/// readable bytes in the buffer, which can indicate a bad message.
58+
case unexpectedEOF
59+
60+
/// This error is thrown when EOF is encountered but there is still an expected next step
61+
/// in the decoder's state machine. This error suggests that the message ended prematurely,
62+
/// possibly indicating a bad message.
63+
case unexpectedNextStep(NextStep)
64+
65+
/// This error is thrown when an unexpected character is encountered in the buffer
66+
/// during the decoding process.
67+
case unexpectedCharacter(UInt8)
68+
}
69+
70+
/// The next step that the decoder will take. The value of this enum determines how the decoder
71+
/// processes the current state of the ByteBuffer.
72+
enum NextStep: Hashable {
73+
/// The initial step.
74+
case returnCode
75+
/// Decode the data length, flags or check if we are the end
76+
case dataLengthOrFlag(MemcachedResponse.ReturnCode)
77+
/// Decode the next flag
78+
case decodeNextFlag(MemcachedResponse.ReturnCode, UInt64?)
79+
// TODO: Add a next step for decoding the response data if the return code is VA
80+
}
81+
82+
/// The action that the decoder will take in response to the current state of the ByteBuffer and the `NextStep`.
83+
enum NextDecodeAction {
84+
/// We need more bytes to decode the next step.
85+
case waitForMoreBytes
86+
/// We can continue decoding.
87+
case continueDecodeLoop
88+
/// We have decoded the next response and need to return it.
89+
case returnDecodedResponse(MemcachedResponse)
90+
}
91+
92+
/// The next step in decoding.
93+
var nextStep: NextStep = .returnCode
94+
95+
mutating func decode(buffer: inout ByteBuffer) throws -> InboundOut? {
96+
while true {
97+
switch try self.next(buffer: &buffer) {
98+
case .returnDecodedResponse(let response):
99+
return response
100+
101+
case .waitForMoreBytes:
102+
return nil
103+
104+
case .continueDecodeLoop:
105+
()
106+
}
107+
}
108+
}
109+
110+
mutating func next(buffer: inout ByteBuffer) throws -> NextDecodeAction {
111+
switch self.nextStep {
112+
case .returnCode:
113+
guard let bytes = buffer.readInteger(as: UInt16.self) else {
114+
return .waitForMoreBytes
115+
}
116+
117+
let returnCode = MemcachedResponse.ReturnCode(bytes)
118+
self.nextStep = .dataLengthOrFlag(returnCode)
119+
return .continueDecodeLoop
120+
121+
case .dataLengthOrFlag(let returnCode):
122+
if returnCode == .VA {
123+
// TODO: Implement decoding of data length
124+
fatalError("Decoding for VA return code is not yet implemented")
125+
}
126+
127+
guard let nextByte = buffer.readInteger(as: UInt8.self) else {
128+
return .waitForMoreBytes
129+
}
130+
131+
if nextByte == UInt8.whitespace {
132+
self.nextStep = .decodeNextFlag(returnCode, nil)
133+
return .continueDecodeLoop
134+
} else if nextByte == UInt8.carriageReturn {
135+
guard let nextNextByte = buffer.readInteger(as: UInt8.self), nextNextByte == UInt8.newline else {
136+
return .waitForMoreBytes
137+
}
138+
let response = MemcachedResponse(returnCode: returnCode, dataLength: nil)
139+
self.nextStep = .returnCode
140+
return .returnDecodedResponse(response)
141+
} else {
142+
throw MemcachedDecoderError.unexpectedCharacter(nextByte)
143+
}
144+
145+
case .decodeNextFlag(let returnCode, let dataLength):
146+
while let nextByte = buffer.readInteger(as: UInt8.self), nextByte != UInt8.whitespace {
147+
// for now consume the byte and do nothing with it.
148+
// TODO: Implement decoding of flags
149+
}
150+
151+
let response = MemcachedResponse(returnCode: returnCode, dataLength: dataLength)
152+
self.nextStep = .returnCode
153+
return .returnDecodedResponse(response)
154+
}
155+
}
156+
157+
mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> MemcachedResponse? {
158+
// Try to decode what is left in the buffer.
159+
if let output = try self.decode(buffer: &buffer) {
160+
return output
161+
}
162+
163+
guard buffer.readableBytes == 0 || seenEOF else {
164+
// If there are still readable bytes left and we haven't seen an EOF
165+
// then something is wrong with the message or how we called the decoder.
166+
throw MemcachedDecoderError.unexpectedEOF
167+
}
168+
169+
switch self.nextStep {
170+
case .returnCode:
171+
return nil
172+
default:
173+
throw MemcachedDecoderError.unexpectedNextStep(self.nextStep)
174+
}
175+
}
176+
}

Tests/SwiftMemcacheTests/IntegrationTest/MemcachedIntegrationTests.swift

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ final class MemcachedIntegrationTest: XCTestCase {
2727
self.channel = ClientBootstrap(group: self.group)
2828
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
2929
.channelInitializer { channel in
30-
channel.pipeline.addHandler(MessageToByteHandler(MemcachedRequestEncoder()))
30+
return channel.pipeline.addHandlers([MessageToByteHandler(MemcachedRequestEncoder()), ByteToMessageHandler(MemcachedResponseDecoder())])
3131
}
3232
}
3333

@@ -36,6 +36,21 @@ final class MemcachedIntegrationTest: XCTestCase {
3636
super.tearDown()
3737
}
3838

39+
class ResponseHandler: ChannelInboundHandler {
40+
typealias InboundIn = MemcachedResponse
41+
42+
let p: EventLoopPromise<MemcachedResponse>
43+
44+
init(p: EventLoopPromise<MemcachedResponse>) {
45+
self.p = p
46+
}
47+
48+
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
49+
let response = self.unwrapInboundIn(data)
50+
self.p.succeed(response)
51+
}
52+
}
53+
3954
func testConnectionToMemcachedServer() throws {
4055
do {
4156
let connection = try channel.connect(host: "memcached", port: 11211).wait()
@@ -47,15 +62,24 @@ final class MemcachedIntegrationTest: XCTestCase {
4762
let command = MemcachedRequest.SetCommand(key: "foo", value: buffer)
4863
let request = MemcachedRequest.set(command)
4964

50-
// Write the request to the connection and wait for the result
51-
connection.writeAndFlush(request).whenComplete { result in
52-
switch result {
53-
case .success:
54-
print("Request successfully sent to the server.")
55-
case .failure(let error):
56-
XCTFail("Failed to send request: \(error)")
57-
}
58-
}
65+
// Write the request to the connection
66+
_ = connection.write(request)
67+
68+
// Prepare the promise for the response
69+
let promise = connection.eventLoop.makePromise(of: MemcachedResponse.self)
70+
let responseHandler = ResponseHandler(p: promise)
71+
_ = connection.pipeline.addHandler(responseHandler)
72+
73+
// Flush and then read the response from the server
74+
connection.flush()
75+
connection.read()
76+
77+
// Wait for the promise to be fulfilled
78+
let response = try promise.futureResult.wait()
79+
80+
// Check the response from the server.
81+
print("Response return code: \(response.returnCode)")
82+
5983
} catch {
6084
XCTFail("Failed to connect to Memcached server: \(error)")
6185
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the swift-memcache-gsoc open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the swift-memcache-gsoc project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of swift-memcache-gsoc project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOEmbedded
17+
@testable import SwiftMemcache
18+
import XCTest
19+
20+
final class MemcachedResponseDecoderTests: XCTestCase {
21+
var decoder: MemcachedResponseDecoder!
22+
23+
override func setUp() {
24+
super.setUp()
25+
self.decoder = MemcachedResponseDecoder()
26+
}
27+
28+
func makeMemcachedResponseByteBuffer(from response: MemcachedResponse) -> ByteBuffer {
29+
var buffer = ByteBufferAllocator().buffer(capacity: 8)
30+
var returnCode: UInt16 = 0
31+
32+
// Convert the return code enum to UInt16 then write it to the buffer.
33+
switch response.returnCode {
34+
case .HD:
35+
returnCode = 0x4844
36+
case .NS:
37+
returnCode = 0x4E53
38+
case .EX:
39+
returnCode = 0x4558
40+
case .NF:
41+
returnCode = 0x4E46
42+
case .VA:
43+
returnCode = 0x5641
44+
}
45+
46+
buffer.writeInteger(returnCode)
47+
48+
// If there's a data length, write it to the buffer.
49+
if let dataLength = response.dataLength, response.returnCode == .VA {
50+
buffer.writeInteger(UInt8.whitespace, as: UInt8.self)
51+
buffer.writeInteger(dataLength, as: UInt64.self)
52+
}
53+
54+
buffer.writeBytes([UInt8.carriageReturn, UInt8.newline])
55+
return buffer
56+
}
57+
58+
func testDecodeResponse(buffer: inout ByteBuffer, expectedReturnCode: MemcachedResponse.ReturnCode) throws {
59+
// Pass our response through the decoder
60+
var output: MemcachedResponse? = nil
61+
do {
62+
output = try self.decoder.decode(buffer: &buffer)
63+
} catch {
64+
XCTFail("Decoding failed with error: \(error)")
65+
}
66+
// Check the decoded response
67+
if let decoded = output {
68+
XCTAssertEqual(decoded.returnCode, expectedReturnCode)
69+
} else {
70+
XCTFail("Failed to decode the inbound response.")
71+
}
72+
}
73+
74+
func testDecodeStoredResponse() throws {
75+
let storedResponse = MemcachedResponse(returnCode: .HD, dataLength: nil)
76+
var buffer = self.makeMemcachedResponseByteBuffer(from: storedResponse)
77+
try self.testDecodeResponse(buffer: &buffer, expectedReturnCode: .HD)
78+
}
79+
80+
func testDecodeNotStoredResponse() throws {
81+
let notStoredResponse = MemcachedResponse(returnCode: .NS, dataLength: nil)
82+
var buffer = self.makeMemcachedResponseByteBuffer(from: notStoredResponse)
83+
try self.testDecodeResponse(buffer: &buffer, expectedReturnCode: .NS)
84+
}
85+
86+
func testDecodeExistResponse() throws {
87+
let existResponse = MemcachedResponse(returnCode: .EX, dataLength: nil)
88+
var buffer = self.makeMemcachedResponseByteBuffer(from: existResponse)
89+
try self.testDecodeResponse(buffer: &buffer, expectedReturnCode: .EX)
90+
}
91+
92+
func testDecodeNotFoundResponse() throws {
93+
let notFoundResponse = MemcachedResponse(returnCode: .NF, dataLength: nil)
94+
var buffer = self.makeMemcachedResponseByteBuffer(from: notFoundResponse)
95+
try self.testDecodeResponse(buffer: &buffer, expectedReturnCode: .NF)
96+
}
97+
}

0 commit comments

Comments
 (0)