Skip to content

Commit bde4eda

Browse files
committed
Add bulk string decoding
1 parent 8fab1fd commit bde4eda

File tree

3 files changed

+118
-3
lines changed

3 files changed

+118
-3
lines changed

Sources/NIORedis/Coders/RedisDataDecoder.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ extension RedisDataDecoder {
5757
case .colon:
5858
guard let number = try _parseInteger(at: &position, from: buffer) else { return .notYetParsed }
5959
return .parsed
60+
case .dollar:
61+
return try _parseBulkString(at: &position, from: buffer)
6062
default: return .notYetParsed
6163
}
6264
}
@@ -103,6 +105,40 @@ extension RedisDataDecoder {
103105

104106
return number
105107
}
108+
109+
/// See https://redis.io/topics/protocol#resp-bulk-strings
110+
func _parseBulkString(at position: inout Int, from buffer: ByteBuffer) throws -> _RedisDataDecodingState {
111+
guard let size = try _parseInteger(at: &position, from: buffer) else { return .notYetParsed }
112+
113+
#warning("TODO: Return null data if null is sent from Redis")
114+
// Redis sends '-1' to represent a null string
115+
guard size > -1 else { return .parsed }
116+
117+
// Redis can hold empty bulk strings, and represents it with a 0 size
118+
// so return an empty string
119+
#warning("TODO: Return an empty bulk string")
120+
guard size > 0 else {
121+
// Move the tip of the message position
122+
// since size = 0, and we successfully parsed the size
123+
// the beginning of the next message should be 2 further (the final \r\n - $0\r\n\r\n)
124+
position += 2
125+
return .parsed
126+
}
127+
128+
// verify that we have at least our expected bulk string message
129+
// by adding the expected CRLF bytes to the parsed size
130+
let readableByteCount = buffer.readableBytes - position
131+
let expectedRemainingMessageSize = size + 2
132+
guard readableByteCount >= expectedRemainingMessageSize else { return .notYetParsed }
133+
134+
guard let bytes = buffer.copyBytes(at: position, length: expectedRemainingMessageSize) else { return .notYetParsed }
135+
136+
// Move the tip of the message position for recursive parsing to just after the newline
137+
// of the bulk string content
138+
position += expectedRemainingMessageSize
139+
140+
return .parsed // bulkString(Data(bytes[ ..<(size - 1) ]))
141+
}
106142
}
107143

108144
private extension ByteBuffer {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Foundation
2+
3+
extension String {
4+
/// Converts this String to a byte representation.
5+
func convertedToData() -> Data { return Data(utf8) }
6+
}

Tests/NIORedisTests/RedisDataDecoderTests.swift

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,53 @@ extension RedisDataDecoderTests {
9999
}
100100
}
101101

102+
// MARK: BulkString Parsing
103+
104+
extension RedisDataDecoderTests {
105+
func testParsing_bulkString_handlesMissingEndings() throws {
106+
XCTAssertEqual(try parseTestBulkString("$6"), .notYetParsed)
107+
XCTAssertEqual(try parseTestBulkString("$6\r\n"), .notYetParsed)
108+
XCTAssertEqual(try parseTestBulkString("$6\r\nabcdef"), .notYetParsed)
109+
}
110+
111+
func testParsing_bulkString_withNoSize_returnsEmpty() throws {
112+
XCTAssertEqual(try parseTestBulkString("$0\r\n"), .parsed)
113+
}
114+
115+
func testParsing_bulkString_withSize_returnsContent() throws {
116+
XCTAssertEqual(try parseTestBulkString("$1\r\n1\r\n"), .parsed)
117+
}
118+
119+
func testParsing_bulkString_withNull_returnsNil() throws {
120+
XCTAssertEqual(try parseTestBulkString("$-1\r\n"), .parsed)
121+
}
122+
123+
func testParsing_bulkString_handlesRawBytes() throws {
124+
let bytes: [UInt8] = [0x00, 0x01, 0x02, 0x03, 0x0A, 0xFF]
125+
let data = "$\(bytes.count)\r\n".convertedToData() + Data(bytes: bytes) + "\r\n".convertedToData()
126+
XCTAssertEqual(try parseTestBulkString(data), .parsed)
127+
}
128+
129+
func testParsing_bulkString_handlesLargeSizes() throws {
130+
let bytes = [UInt8].init(repeating: .dollar, count: 10_000_000)
131+
let data = "$\(bytes.count)\r\n".convertedToData() + Data(bytes: bytes) + "\r\n".convertedToData()
132+
XCTAssertEqual(try parseTestBulkString(data), .parsed)
133+
}
134+
135+
private func parseTestBulkString(_ input: String) throws -> RedisDataDecoder._RedisDataDecodingState {
136+
return try parseTestBulkString(input.convertedToData())
137+
}
138+
139+
private func parseTestBulkString(_ input: Data) throws -> RedisDataDecoder._RedisDataDecodingState {
140+
var buffer = allocator.buffer(capacity: input.count)
141+
buffer.write(bytes: input)
142+
143+
var position = 1 // "trim" token
144+
145+
return try RedisDataDecoder()._parseBulkString(at: &position, from: buffer)
146+
}
147+
}
148+
102149
// MARK: Message Parsing
103150

104151
extension RedisDataDecoderTests {
@@ -118,10 +165,28 @@ extension RedisDataDecoderTests {
118165
try parseTest_recursive(withChunks: [":300\r", "\n:-10135135\r", "\n:1\r", "\n"])
119166
}
120167

168+
func testParsing_with_bulkString() throws {
169+
try parseTest_singleValue(input: "$-1\r")
170+
try parseTest_singleValue(input: "$0\r")
171+
try parseTest_singleValue(input: "$1\r\n!\r")
172+
try parseTest_singleValue(input: "$1\r\n".convertedToData() + Data(bytes: [0xff] + "\r".convertedToData()))
173+
}
174+
175+
func testParsing_with_bulkString_recursively() throws {
176+
try parseTest_recursive(withChunks: ["$3\r", "\naaa\r\n$", "4\r\nnio!\r\n"])
177+
}
178+
179+
/// See parse_Test_singleValue(input:) String
121180
private func parseTest_singleValue(input: String) throws {
181+
try parseTest_singleValue(input: input.convertedToData())
182+
}
183+
184+
/// Takes a collection of bytes representing an incomplete message to assert decoding states
185+
/// This method will add the appropriate \n terminator to the end of the byte stream
186+
private func parseTest_singleValue(input: Data) throws {
122187
let decoder = RedisDataDecoder()
123188
var buffer = allocator.buffer(capacity: input.count + 1)
124-
buffer.write(string: input)
189+
buffer.write(bytes: input)
125190

126191
var position = 0
127192

@@ -133,10 +198,18 @@ extension RedisDataDecoderTests {
133198
XCTAssertEqual(try decoder._parse(at: &position, from: buffer), .parsed)
134199
}
135200

201+
/// See parseTest_recursive(withCunks:) [Data]
136202
private func parseTest_recursive(withChunks messageChunks: [String]) throws {
203+
try parseTest_recursive(withChunks: messageChunks.map({ $0.convertedToData() }))
204+
}
205+
206+
/// Takes a collection of byte streams to write to a buffer and assert decoding states in between
207+
/// buffer writes.
208+
/// The expected pattern of messages should be [incomplete, remaining, incomplete, remaining]
209+
private func parseTest_recursive(withChunks messageChunks: [Data]) throws {
137210
let decoder = RedisDataDecoder()
138211
var buffer = allocator.buffer(capacity: messageChunks.joined().count)
139-
buffer.write(string: messageChunks[0])
212+
buffer.write(bytes: messageChunks[0])
140213

141214
var position = 0
142215

@@ -145,7 +218,7 @@ extension RedisDataDecoderTests {
145218
for index in 1..<messageChunks.count {
146219
position = 0
147220

148-
buffer.write(string: messageChunks[index])
221+
buffer.write(bytes: messageChunks[index])
149222

150223
XCTAssertEqual(try decoder._parse(at: &position, from: buffer), .parsed)
151224

0 commit comments

Comments
 (0)