Skip to content

Commit ed88f85

Browse files
authored
Implement a get request (#14)
* init get request structure * re work MemcachedFlags model * dataLengthOrFlag split + readMemcachedFlags * decoder pr refactors * write and read memcached flag fixes * combine flag states into one * data length is written as text not encoded as UInt64 * remaining PR changes * optimized dataLength read by fixing readIntegerFromASCII * removed flagToByte prop * final clean up closes #6 * flag state clean up + set up for partial buffers impl to come soon * closes #6 * ensure we have \r\n before parsing * \r\n + partial/split buffer test * closes #6 * .suffix(2) is costly removed it * walk buffer to find first \r\n
1 parent d1a872c commit ed88f85

11 files changed

+300
-60
lines changed

Sources/SwiftMemcache/Extensions/ByteBuffer+SwiftMemcache.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,67 @@ extension ByteBuffer {
2525
self.writeString(string)
2626
}
2727
}
28+
29+
extension ByteBuffer {
30+
/// Reads an integer from ASCII characters directly from this `ByteBuffer`.
31+
/// The reading stops as soon as a non-digit character is encountered.
32+
///
33+
/// - Returns: A `UInt64` integer read from the buffer.
34+
/// If the buffer does not contain any digits at the current reading position, returns `nil`.
35+
mutating func readIntegerFromASCII() -> UInt64? {
36+
var value: UInt64 = 0
37+
while self.readableBytes > 0, let currentByte = self.readInteger(as: UInt8.self),
38+
currentByte >= UInt8.zero && currentByte <= UInt8.nine {
39+
value = (value * 10) + UInt64(currentByte - UInt8.zero)
40+
}
41+
return value > 0 ? value : nil
42+
}
43+
}
44+
45+
extension ByteBuffer {
46+
/// Serialize and writes MemcachedFlags to the ByteBuffer.
47+
///
48+
/// This method runs a loop over the flags contained in a MemcachedFlags instance.
49+
/// For each flag that is set to true, its corresponding byte value,
50+
/// followed by a whitespace character, is added to the ByteBuffer.
51+
///
52+
/// - parameters:
53+
/// - flags: An instance of MemcachedFlags that holds the flags intended to be serialized and written to the ByteBuffer.
54+
mutating func writeMemcachedFlags(flags: MemcachedFlags) {
55+
if let shouldReturnValue = flags.shouldReturnValue, shouldReturnValue {
56+
self.writeInteger(UInt8.whitespace)
57+
self.writeInteger(UInt8.v)
58+
}
59+
}
60+
}
61+
62+
extension ByteBuffer {
63+
/// Parses flags from this `ByteBuffer`, advancing the reader index accordingly.
64+
///
65+
/// - returns: A `MemcachedFlags` instance populated with the flags read from the buffer.
66+
mutating func readMemcachedFlags() -> MemcachedFlags {
67+
let flags = MemcachedFlags()
68+
while let nextByte = self.getInteger(at: self.readerIndex, as: UInt8.self) {
69+
switch nextByte {
70+
case UInt8.whitespace:
71+
self.moveReaderIndex(forwardBy: 1)
72+
continue
73+
case UInt8.carriageReturn:
74+
guard let followingByte = self.getInteger(at: self.readerIndex + 1, as: UInt8.self) else {
75+
// We were expecting a newline after the carriage return, but didn't get it.
76+
fatalError("Unexpected end of flags. Expected newline after carriage return.")
77+
}
78+
if followingByte == UInt8.newline {
79+
self.moveReaderIndex(forwardBy: 2)
80+
} else {
81+
// If it wasn't a newline, it is something unexpected.
82+
fatalError("Unexpected character in flags. Expected newline after carriage return.")
83+
}
84+
default:
85+
// Encountered a character we weren't expecting. This should be a fatal error.
86+
fatalError("Unexpected character in flags.")
87+
}
88+
}
89+
return flags
90+
}
91+
}

Sources/SwiftMemcache/Extensions/UInt8+Characters.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ extension UInt8 {
1818
static var carriageReturn: UInt8 = .init(ascii: "\r")
1919
static var m: UInt8 = .init(ascii: "m")
2020
static var s: UInt8 = .init(ascii: "s")
21+
static var g: UInt8 = .init(ascii: "g")
22+
static var v: UInt8 = .init(ascii: "v")
23+
static var zero: UInt8 = .init(ascii: "0")
24+
static var nine: UInt8 = .init(ascii: "9")
2125
}

Sources/SwiftMemcache/MemcachedFlag.swift

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 representing the flags of a Memcached command.
16+
///
17+
/// Flags for the 'mg' (meta get) command are represented in this struct.
18+
/// Currently, only the 'v' flag for the meta get command is supported,
19+
/// which dictates whether the item value should be returned in the data block.
20+
struct MemcachedFlags {
21+
/// Flag 'v' for the 'mg' (meta get) command.
22+
///
23+
/// If true, the item value is returned in the data block.
24+
/// If false, the data block for the 'mg' response is optional, and the response code changes from "HD" to "VA <size>".
25+
var shouldReturnValue: Bool?
26+
27+
init() {}
28+
}
29+
30+
extension MemcachedFlags: Hashable {}

Sources/SwiftMemcache/MemcachedRequest.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,11 @@ enum MemcachedRequest {
2020
var value: ByteBuffer
2121
}
2222

23+
struct GetCommand {
24+
let key: String
25+
var flags: MemcachedFlags
26+
}
27+
2328
case set(SetCommand)
29+
case get(GetCommand)
2430
}

Sources/SwiftMemcache/MemcachedRequestEncoder.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ struct MemcachedRequestEncoder: MessageToByteEncoder {
4242
out.writeBuffer(&command.value)
4343
out.writeInteger(UInt8.carriageReturn)
4444
out.writeInteger(UInt8.newline)
45+
46+
case .get(let command):
47+
precondition(!command.key.isEmpty, "Key must not be empty")
48+
49+
// write command and key
50+
out.writeInteger(UInt8.m)
51+
out.writeInteger(UInt8.g)
52+
out.writeInteger(UInt8.whitespace)
53+
out.writeBytes(command.key.utf8)
54+
55+
// write flags if there are any
56+
out.writeMemcachedFlags(flags: command.flags)
57+
58+
// write separator
59+
out.writeInteger(UInt8.carriageReturn)
60+
out.writeInteger(UInt8.newline)
4561
}
4662
}
4763
}

Sources/SwiftMemcache/MemcachedResponse.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15+
import NIOCore
16+
1517
struct MemcachedResponse {
1618
enum ReturnCode {
1719
case HD
@@ -40,4 +42,6 @@ struct MemcachedResponse {
4042

4143
var returnCode: ReturnCode
4244
var dataLength: UInt64?
45+
var flags: MemcachedFlags?
46+
var value: ByteBuffer?
4347
}

Sources/SwiftMemcache/MemcachedResponseDecoder.swift

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,10 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
5656
/// This error is thrown when EOF is encountered but there are still
5757
/// readable bytes in the buffer, which can indicate a bad message.
5858
case unexpectedEOF
59-
6059
/// This error is thrown when EOF is encountered but there is still an expected next step
6160
/// in the decoder's state machine. This error suggests that the message ended prematurely,
6261
/// possibly indicating a bad message.
6362
case unexpectedNextStep(NextStep)
64-
6563
/// This error is thrown when an unexpected character is encountered in the buffer
6664
/// during the decoding process.
6765
case unexpectedCharacter(UInt8)
@@ -72,11 +70,12 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
7270
enum NextStep: Hashable {
7371
/// The initial step.
7472
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
73+
/// Decode the data length
74+
case dataLength(MemcachedResponse.ReturnCode)
75+
/// Decode the flags
76+
case decodeFlag(MemcachedResponse.ReturnCode, UInt64?)
77+
/// Decode the Value
78+
case decodeValue(MemcachedResponse.ReturnCode, UInt64, MemcachedFlags)
8079
}
8180

8281
/// The action that the decoder will take in response to the current state of the ByteBuffer and the `NextStep`.
@@ -108,47 +107,84 @@ struct MemcachedResponseDecoder: NIOSingleStepByteToMessageDecoder {
108107
}
109108

110109
mutating func next(buffer: inout ByteBuffer) throws -> NextDecodeAction {
110+
// Check if the buffer contains "\r\n"
111+
let bytesView = buffer.readableBytesView
112+
guard let crIndex = bytesView.firstIndex(of: UInt8.carriageReturn), bytesView.index(after: crIndex) < bytesView.endIndex,
113+
bytesView[bytesView.index(after: crIndex)] == UInt8.newline else {
114+
return .waitForMoreBytes
115+
}
111116
switch self.nextStep {
112117
case .returnCode:
113118
guard let bytes = buffer.readInteger(as: UInt16.self) else {
114119
return .waitForMoreBytes
115120
}
116121

117122
let returnCode = MemcachedResponse.ReturnCode(bytes)
118-
self.nextStep = .dataLengthOrFlag(returnCode)
123+
self.nextStep = .dataLength(returnCode)
119124
return .continueDecodeLoop
120125

121-
case .dataLengthOrFlag(let returnCode):
126+
case .dataLength(let returnCode):
122127
if returnCode == .VA {
123-
// TODO: Implement decoding of data length
124-
fatalError("Decoding for VA return code is not yet implemented")
128+
// Check if we have at least one whitespace in the buffer.
129+
guard buffer.readableBytesView.contains(UInt8.whitespace) else {
130+
return .waitForMoreBytes
131+
}
132+
133+
if let currentByte = buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), currentByte == UInt8.whitespace {
134+
buffer.moveReaderIndex(forwardBy: 1)
135+
}
136+
137+
guard let dataLength = buffer.readIntegerFromASCII() else {
138+
throw MemcachedDecoderError.unexpectedCharacter(buffer.readableBytesView[buffer.readerIndex])
139+
}
140+
141+
self.nextStep = .decodeFlag(returnCode, dataLength)
142+
return .continueDecodeLoop
143+
} else {
144+
self.nextStep = .decodeFlag(returnCode, nil)
145+
return .continueDecodeLoop
125146
}
126147

127-
guard let nextByte = buffer.readInteger(as: UInt8.self) else {
128-
return .waitForMoreBytes
148+
case .decodeFlag(let returnCode, let dataLength):
149+
if let nextByte = buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), nextByte == UInt8.newline {
150+
self.nextStep = .decodeValue(returnCode, dataLength!, MemcachedFlags())
151+
buffer.moveReaderIndex(forwardBy: 1)
152+
return .continueDecodeLoop
153+
}
154+
155+
if let currentByte = buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), currentByte == UInt8.whitespace {
156+
buffer.moveReaderIndex(forwardBy: 1)
129157
}
130158

131-
if nextByte == UInt8.whitespace {
132-
self.nextStep = .decodeNextFlag(returnCode, nil)
159+
let flags = buffer.readMemcachedFlags()
160+
161+
if returnCode == .VA {
162+
self.nextStep = .decodeValue(returnCode, dataLength!, flags)
133163
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)
164+
} else {
165+
let response = MemcachedResponse(returnCode: returnCode, dataLength: dataLength, flags: flags, value: nil)
139166
self.nextStep = .returnCode
140167
return .returnDecodedResponse(response)
141-
} else {
142-
throw MemcachedDecoderError.unexpectedCharacter(nextByte)
143168
}
144169

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
170+
case .decodeValue(let returnCode, let dataLength, let flags):
171+
guard buffer.readableBytes >= dataLength + 2 else {
172+
return .waitForMoreBytes
173+
}
174+
175+
guard let data = buffer.readSlice(length: Int(dataLength)) else {
176+
throw MemcachedDecoderError.unexpectedEOF
177+
}
178+
179+
guard buffer.readableBytes >= 2,
180+
let nextByte = buffer.readInteger(as: UInt8.self),
181+
nextByte == UInt8.carriageReturn,
182+
let nextNextByte = buffer.readInteger(as: UInt8.self),
183+
nextNextByte == UInt8.newline else {
184+
preconditionFailure("Expected to find CRLF at end of response")
149185
}
150186

151-
let response = MemcachedResponse(returnCode: returnCode, dataLength: dataLength)
187+
let response = MemcachedResponse(returnCode: returnCode, dataLength: dataLength, flags: flags, value: data)
152188
self.nextStep = .returnCode
153189
return .returnDecodedResponse(response)
154190
}

Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagTests.swift renamed to Tests/SwiftMemcacheTests/UnitTest/MemcachedFlagsTests.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@
1515
@testable import SwiftMemcache
1616
import XCTest
1717

18-
final class MemcachedFlagTests: XCTestCase {
19-
func testVFlagBytes() {
20-
let flag = MemcachedFlag.v
21-
XCTAssertEqual(flag.bytes, 0x76)
18+
final class MemcachedFlagsTests: XCTestCase {
19+
func testVFlag() {
20+
var flags = MemcachedFlags()
21+
flags.shouldReturnValue = true
22+
if let shouldReturnValue = flags.shouldReturnValue {
23+
XCTAssertTrue(shouldReturnValue)
24+
} else {
25+
XCTFail("Flag shouldReturnValue is nil")
26+
}
2227
}
2328
}

Tests/SwiftMemcacheTests/UnitTest/MemcachedRequestEncoderTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,24 @@ final class MemcachedRequestEncoderTests: XCTestCase {
4242
let expectedEncodedData = "ms foo 2\r\nhi\r\n"
4343
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
4444
}
45+
46+
func testEncodeGetRequest() {
47+
// Prepare a MemcachedRequest
48+
var flags = MemcachedFlags()
49+
flags.shouldReturnValue = true
50+
let command = MemcachedRequest.GetCommand(key: "foo", flags: flags)
51+
52+
let request = MemcachedRequest.get(command)
53+
54+
// Pass our request through the encoder
55+
var outBuffer = ByteBufferAllocator().buffer(capacity: 0)
56+
do {
57+
try self.encoder.encode(data: request, out: &outBuffer)
58+
} catch {
59+
XCTFail("Encoding failed with error: \(error)")
60+
}
61+
62+
let expectedEncodedData = "mg foo v\r\n"
63+
XCTAssertEqual(outBuffer.getString(at: 0, length: outBuffer.readableBytes), expectedEncodedData)
64+
}
4565
}

0 commit comments

Comments
 (0)