- Notifications
You must be signed in to change notification settings - Fork 9
Created a decoder handler to deserialize a MemcachedResponse #11
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
Changes from 1 commit
2838923 4ce410d c2e44ec 5624259 633e79b 067a943 6ebbea2 72b883e abb43c8 077d046 9a4f3be 7e385c3 b7772ce 250f680 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -15,84 +15,167 @@ | |
| import NIOCore | ||
| import NIOPosix | ||
| | ||
| /// Responses look like: | ||
| /// | ||
| /// <RC> <datalen*> <flag1> <flag2> <...>\r\n | ||
| /// | ||
| /// Where <RC> is a 2 character return code. The number of flags returned are | ||
| /// based off of the flags supplied. | ||
| /// | ||
| /// <datalen> is only for responses with payloads, with the return code 'VA'. | ||
| /// | ||
| /// Flags are single character codes, ie 'q' or 'k' or 'I', which adjust the | ||
| /// behavior of the command. If a flag requests a response flag (ie 't' for TTL | ||
| /// remaining), it is returned in the same order as they were in the original | ||
| /// command, though this is not strict. | ||
| /// | ||
| /// Flags are single character codes, ie 'q' or 'k' or 'O', which adjust the | ||
| /// behavior of a command. Flags may contain token arguments, which come after the | ||
| /// flag and before the next space or newline, ie 'Oopaque' or 'Kuserkey'. Flags | ||
| /// can return new data or reflect information, in the same order they were | ||
| /// supplied in the request. Sending an 't' flag with a get for an item with 20 | ||
| /// seconds of TTL remaining, would return 't20' in the response. | ||
| /// | ||
| /// All commands accept a tokens 'P' and 'L' which are completely ignored. The | ||
| /// arguments to 'P' and 'L' can be used as hints or path specifications to a | ||
| /// proxy or router inbetween a client and a memcached daemon. For example, a | ||
| /// client may prepend a "path" in the key itself: "mg /path/foo v" or in a proxy | ||
| /// token: "mg foo Lpath/ v" - the proxy may then optionally remove or forward the | ||
| /// token to a memcached daemon, which will ignore them. | ||
| /// | ||
| /// Syntax errors are handled the same as noted under 'Error strings' section | ||
| /// below. | ||
| /// | ||
| /// For usage examples beyond basic syntax, please see the wiki: | ||
| /// https://github.com/memcached/memcached/wiki/MetaCommands | ||
| struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder { | ||
| /// Responses look like: | ||
| /// | ||
| /// <RC> <datalen*> <flag1> <flag2> <...>\r\n | ||
| /// | ||
| /// Where <RC> is a 2 character return code. The number of flags returned are | ||
| /// based off of the flags supplied. | ||
| /// | ||
| /// <datalen> is only for responses with payloads, with the return code 'VA'. | ||
| /// | ||
| /// Flags are single character codes, ie 'q' or 'k' or 'I', which adjust the | ||
| /// behavior of the command. If a flag requests a response flag (ie 't' for TTL | ||
| /// remaining), it is returned in the same order as they were in the original | ||
| /// command, though this is not strict. | ||
| /// | ||
| /// Flags are single character codes, ie 'q' or 'k' or 'O', which adjust the | ||
| /// behavior of a command. Flags may contain token arguments, which come after the | ||
| /// flag and before the next space or newline, ie 'Oopaque' or 'Kuserkey'. Flags | ||
| /// can return new data or reflect information, in the same order they were | ||
| /// supplied in the request. Sending an 't' flag with a get for an item with 20 | ||
| /// seconds of TTL remaining, would return 't20' in the response. | ||
| /// | ||
| /// All commands accept a tokens 'P' and 'L' which are completely ignored. The | ||
| /// arguments to 'P' and 'L' can be used as hints or path specifications to a | ||
| /// proxy or router inbetween a client and a memcached daemon. For example, a | ||
| /// client may prepend a "path" in the key itself: "mg /path/foo v" or in a proxy | ||
| /// token: "mg foo Lpath/ v" - the proxy may then optionally remove or forward the | ||
| /// token to a memcached daemon, which will ignore them. | ||
| /// | ||
| /// Syntax errors are handled the same as noted under 'Error strings' section | ||
| /// below. | ||
| /// | ||
| /// For usage examples beyond basic syntax, please see the wiki: | ||
| /// https://github.com/memcached/memcached/wiki/MetaCommands | ||
| typealias InboundOut = MemcachedResponse | ||
| | ||
| func decode(buffer: inout ByteBuffer) throws -> InboundOut? { | ||
| // Ensure the buffer has at least 3 bytes (minimum for a response code and newline) | ||
| guard buffer.readableBytes >= 3 else { | ||
| return nil // Need more data | ||
| } | ||
| /// Describes the errors that can occur during the decoding process. | ||
| enum MemcachedDecoderError: Error { | ||
| /// This error is thrown when EOF is encountered but there are still | ||
| /// readable bytes in the buffer, which can indicate a bad message. | ||
| case unexpectedEOF | ||
| | ||
| // Read the first two characters | ||
| guard let firstReturnCode = buffer.readInteger(as: UInt8.self), | ||
| let secondReturnCode = buffer.readInteger(as: UInt8.self) else { | ||
| preconditionFailure("Response code could not be read.") | ||
| } | ||
| /// This error is thrown when EOF is encountered but the decoder's next step | ||
| /// is not `.none`. This error suggests that the message ended prematurely, | ||
| /// possibly indicating a bad message. | ||
| case unexpectedNextStep(NextStep) | ||
| } | ||
| | ||
| /// The next step that the decoder will take. The value of this enum determines how the decoder | ||
| /// processes the current state of the ByteBuffer. | ||
| enum NextStep: Hashable { | ||
| /// No further steps are needed, the decoding process is complete for the current message. | ||
| case none | ||
| /// The initial step. | ||
| case returnCode | ||
| /// Decode the data length, flags or check if we are the end | ||
| case dataLengthOrFlag(MemcachedResponse.ReturnCode) | ||
| /// Decode the next flag | ||
| case decodeNextFlag(MemcachedResponse.ReturnCode, UInt64?, [UInt8]) | ||
| /// Decode end of line | ||
| case decodeEndOfLine(MemcachedResponse.ReturnCode, UInt64?, [UInt8]) | ||
| } | ||
| | ||
| /// The action that the decoder will take in response to the current state of the ByteBuffer and the `NextStep`. | ||
| enum NextDecodeAction { | ||
| /// We need more bytes to decode the next step. | ||
| case waitForMoreBytes | ||
| /// We can continue decoding. | ||
| case continueDecodeLoop | ||
| /// We have decoded the next response and need to return it. | ||
| case returnDecodedResponse(MemcachedResponse) | ||
| } | ||
| | ||
| let returnCode = MemcachedResponse.ReturnCode( | ||
| UInt16(firstReturnCode) << 8 | UInt16(secondReturnCode) | ||
| ) | ||
| /// The next step in decoding. | ||
| var nextStep: NextStep = .returnCode | ||
| | ||
| // If there is not a whitespace, then we are at the end of the line. | ||
| guard buffer.readableBytes > 0, let nextByte = buffer.getInteger(at: buffer.readerIndex, as: UInt8.self) else { | ||
| return nil // Need more dat | ||
| mutating func decode(buffer: inout ByteBuffer) throws -> InboundOut? { | ||
| while self.nextStep != .none { | ||
| switch try self.next(buffer: &buffer) { | ||
| case .returnDecodedResponse(let response): | ||
| return response | ||
| | ||
| case .waitForMoreBytes: | ||
| return nil | ||
| | ||
| case .continueDecodeLoop: | ||
| () | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| | ||
| if nextByte != UInt8.whitespace { | ||
| // We're at the end of the line | ||
| buffer.moveReaderIndex(forwardBy: 1) | ||
| } else { | ||
| // We have additional data or flags to read | ||
| buffer.moveReaderIndex(forwardBy: 1) | ||
| | ||
| // Assert that we really read \r\n | ||
| guard buffer.readableBytes >= 2, | ||
| buffer.getInteger(at: buffer.readerIndex, as: UInt8.self) == UInt8.carriageReturn, | ||
| buffer.getInteger(at: buffer.readerIndex + 1, as: UInt8.self) == UInt8.newline else { | ||
| preconditionFailure("Response ending '\r\n' not found.") | ||
| mutating func next(buffer: inout ByteBuffer) throws -> NextDecodeAction { | ||
| switch self.nextStep { | ||
| case .none: | ||
| return .waitForMoreBytes | ||
| | ||
| case .returnCode: | ||
| guard let bytes = buffer.readInteger(as: UInt16.self) else { | ||
| return .waitForMoreBytes | ||
| } | ||
| | ||
| let returnCode = MemcachedResponse.ReturnCode(bytes) | ||
| self.nextStep = .dataLengthOrFlag(returnCode) | ||
| return .continueDecodeLoop | ||
| | ||
| case .dataLengthOrFlag(let returnCode): | ||
| if returnCode == .stored { | ||
| ||
| // TODO: Implement decoding of data length | ||
FranzBusch marked this conversation as resolved. Show resolved Hide resolved | ||
| } | ||
| | ||
| // Check if the next bytes are \r\n | ||
| ||
| if buffer.consumeEndOfLine() { | ||
| let response = MemcachedResponse(returnCode: returnCode, dataLength: nil, flags: []) | ||
| self.nextStep = .returnCode | ||
| return .returnDecodedResponse(response) | ||
| } else { | ||
FranzBusch marked this conversation as resolved. Show resolved Hide resolved | ||
| self.nextStep = .decodeNextFlag(returnCode, nil, []) | ||
| return .continueDecodeLoop | ||
| } | ||
| | ||
| case .decodeNextFlag(let returnCode, let dataLength, var flags): | ||
| if let nextByte = buffer.readInteger(as: UInt8.self), nextByte != UInt8.whitespace { | ||
| flags.append(nextByte) | ||
| ||
| self.nextStep = .decodeNextFlag(returnCode, dataLength, flags) | ||
| return .continueDecodeLoop | ||
| } else { | ||
| self.nextStep = .decodeEndOfLine(returnCode, dataLength, flags) | ||
| return .continueDecodeLoop | ||
| } | ||
| | ||
| case .decodeEndOfLine(let returnCode, let dataLength, let flags): | ||
| guard buffer.consumeEndOfLine() else { | ||
| return .waitForMoreBytes | ||
| } | ||
| | ||
| // Skip the CRLF | ||
| buffer.moveReaderIndex(forwardBy: 2) | ||
| let response = MemcachedResponse(returnCode: returnCode, dataLength: dataLength, flags: flags) | ||
| self.nextStep = .returnCode | ||
| return .returnDecodedResponse(response) | ||
| } | ||
| | ||
| return MemcachedResponse(returnCode: returnCode) | ||
| } | ||
| | ||
| func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> InboundOut? { | ||
| return try self.decode(buffer: &buffer) | ||
| mutating func decodeLast(buffer: inout ByteBuffer, seenEOF: Bool) throws -> MemcachedResponse? { | ||
| // Try to decode what is left in the buffer. | ||
| if let output = try self.decode(buffer: &buffer) { | ||
| return output | ||
| } | ||
| | ||
| guard buffer.readableBytes == 0 || seenEOF else { | ||
| // If there are still readable bytes left and we haven't seen an EOF | ||
| // then something is wrong with the message or how we called the decoder. | ||
| throw MemcachedDecoderError.unexpectedEOF | ||
| } | ||
| | ||
| switch self.nextStep { | ||
| case .none, .returnCode: | ||
| return nil | ||
| default: | ||
| throw MemcachedDecoderError.unexpectedNextStep(self.nextStep) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we never transition to that state and since our state machine is looping we can get rid of this