Skip to content

Commit 755f631

Browse files
authored
Merge pull request #34 from Mordil/bytebuffers
Rework RESPValue to use ByteBuffers for .simpleString and .bulkString
2 parents 741482d + d93511b commit 755f631

File tree

10 files changed

+318
-251
lines changed

10 files changed

+318
-251
lines changed

Sources/NIORedis/ChannelHandlers/RESPDecoder+B2MD.swift

Lines changed: 0 additions & 26 deletions
This file was deleted.

Sources/NIORedis/ChannelHandlers/RESPEncoder+M2BE.swift

Lines changed: 0 additions & 21 deletions
This file was deleted.

Sources/NIORedis/RESP/RESPDecoder.swift

Lines changed: 93 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,6 @@ extension UInt8 {
1010
static let colon: UInt8 = 0x3A
1111
}
1212

13-
private extension ByteBuffer {
14-
/// Copies bytes from the `ByteBuffer` from at the provided position, up to the length desired.
15-
///
16-
/// buffer.copyBytes(at: 3, length: 2)
17-
/// // Optional(2 bytes), assuming buffer contains 5 bytes
18-
///
19-
/// - Parameters:
20-
/// - at: The position offset to copy bytes from the buffer, defaulting to `0`.
21-
/// - length: The number of bytes to copy.
22-
func copyBytes(at offset: Int = 0, length: Int) -> [UInt8]? {
23-
guard readableBytes >= offset + length else { return nil }
24-
return getBytes(at: offset + readerIndex, length: length)
25-
}
26-
}
27-
2813
/// Handles incoming byte messages from Redis and decodes them according to the RESP protocol.
2914
///
3015
/// See: [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
@@ -42,150 +27,157 @@ public final class RESPDecoder {
4227
///
4328
/// See [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
4429
/// - Parameters:
45-
/// - at: The index of the buffer that should be considered the "front" to begin message parsing.
46-
/// - from: The buffer that contains the bytes that need to be decoded.
47-
public func parse(at position: inout Int, from buffer: inout ByteBuffer) throws -> ParsingState {
48-
guard let token = buffer.copyBytes(at: position, length: 1)?.first else { return .notYetParsed }
30+
/// - buffer: The buffer that contains the bytes that need to be decoded.
31+
/// - position: The index of the buffer that should be considered the "front" to begin message parsing.
32+
public func parse(from buffer: inout ByteBuffer, index position: inout Int) throws -> ParsingState {
33+
let offset = position + buffer.readerIndex
34+
guard
35+
let token = buffer.viewBytes(at: offset, length: 1)?.first,
36+
var slice = buffer.getSlice(at: offset, length: buffer.readableBytes - position)
37+
else { return .notYetParsed }
4938

5039
position += 1
5140

5241
switch token {
5342
case .plus:
54-
guard let string = try _parseSimpleString(at: &position, from: &buffer) else { return .notYetParsed }
55-
return .parsed(.simpleString(string))
43+
guard let result = parseSimpleString(&slice, &position) else { return .notYetParsed }
44+
return .parsed(.simpleString(result))
5645

5746
case .colon:
58-
guard let number = try _parseInteger(at: &position, from: &buffer) else { return .notYetParsed }
59-
return .parsed(.integer(number))
47+
guard let value = parseInteger(&slice, &position) else { return .notYetParsed }
48+
return .parsed(.integer(value))
6049

6150
case .dollar:
62-
return try _parseBulkString(at: &position, from: &buffer)
51+
return parseBulkString(&slice, &position)
6352

6453
case .asterisk:
65-
return try _parseArray(at: &position, from: &buffer)
54+
return try parseArray(&slice, &position)
6655

6756
case .hyphen:
68-
guard let string = try _parseSimpleString(at: &position, from: &buffer) else { return .notYetParsed }
69-
return .parsed(.error(RedisError(identifier: "serverSide", reason: string)))
70-
71-
default:
72-
throw RedisError(
73-
identifier: "invalidTokenType",
74-
reason: "Unexpected error while parsing Redis RESP."
75-
)
57+
guard
58+
let stringBuffer = parseSimpleString(&slice, &position),
59+
let message = stringBuffer.getString(at: 0, length: stringBuffer.readableBytes)
60+
else { return .notYetParsed }
61+
return .parsed(.error(RedisError(identifier: "serverSide", reason: message)))
62+
63+
default: throw RedisError(identifier: "invalidTokenType", reason: "Unexpected error while parsing Redis RESP.")
7664
}
7765
}
7866
}
7967

80-
// Parsing
68+
extension RESPDecoder: ByteToMessageDecoder {
69+
/// `ByteToMessageDecoder.InboundOut`
70+
public typealias InboundOut = RESPValue
8171

82-
extension RESPDecoder {
83-
/// See [https://redis.io/topics/protocol#resp-simple-strings](https://redis.io/topics/protocol#resp-simple-strings)
84-
func _parseSimpleString(at position: inout Int, from buffer: inout ByteBuffer) throws -> String? {
85-
let byteCount = buffer.readableBytes - position
86-
guard
87-
byteCount >= 2, // strings should at least have a CRLF line ending
88-
let bytes = buffer.copyBytes(at: position, length: byteCount)
89-
else { return nil }
72+
/// See `ByteToMessageDecoder.decode(context:buffer:)`
73+
public func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
74+
var position = 0
9075

91-
// String endings have a return carriage followed by a newline
92-
// Search for the first \r and to find the expected newline offset
93-
var expectedNewlinePosition = 0
94-
for offset in 0..<bytes.count {
95-
if bytes[offset] == .carriageReturn {
96-
expectedNewlinePosition = offset + 1
97-
break
98-
}
76+
switch try parse(from: &buffer, index: &position) {
77+
case .notYetParsed: return .needMoreData
78+
case let .parsed(value):
79+
context.fireChannelRead(wrapInboundOut(value))
80+
buffer.moveReaderIndex(forwardBy: position)
81+
return .continue
9982
}
83+
}
84+
85+
/// See `ByteToMessageDecoder.decodeLast(context:buffer:seenEOF)`
86+
public func decodeLast(
87+
context: ChannelHandlerContext,
88+
buffer: inout ByteBuffer,
89+
seenEOF: Bool
90+
) throws -> DecodingState { return .needMoreData }
91+
}
10092

101-
// Make sure the position is still within readable range, and that the position reality matches our
102-
// expectation
93+
// MARK: Parsing
94+
95+
extension RESPDecoder {
96+
/// See [https://redis.io/topics/protocol#resp-simple-strings](https://redis.io/topics/protocol#resp-simple-strings)
97+
func parseSimpleString(_ buffer: inout ByteBuffer, _ position: inout Int) -> ByteBuffer? {
10398
guard
104-
expectedNewlinePosition < bytes.count,
105-
bytes[expectedNewlinePosition] == .newline
99+
let bytes = buffer.viewBytes(at: position, length: buffer.readableBytes - position),
100+
let newlineIndex = bytes.firstIndex(of: .newline),
101+
newlineIndex >= (position - bytes.startIndex) + 2 // strings should at least have a CRLF line ending
106102
else { return nil }
107103

108-
// Move the tip of the message position for recursive parsing to just after the newline
109-
position += expectedNewlinePosition + 1
104+
// move the parsing position to the newline for recursive parsing
105+
position += newlineIndex
110106

111-
return String(bytes: bytes[ ..<(expectedNewlinePosition - 1) ], encoding: .utf8)
107+
// the end of the message will be just before the newlineIndex,
108+
// offset by the view's startIndex
109+
return buffer.getSlice(at: bytes.startIndex, length: (newlineIndex - 1) - bytes.startIndex)
112110
}
113111

114112
/// See [https://redis.io/topics/protocol#resp-integers](https://redis.io/topics/protocol#resp-integers)
115-
func _parseInteger(at position: inout Int, from buffer: inout ByteBuffer) throws -> Int? {
116-
guard let string = try _parseSimpleString(at: &position, from: &buffer) else { return nil }
117-
118-
guard let number = Int(string) else {
119-
throw RedisError(
120-
identifier: "parseInteger",
121-
reason: "Unexpected error while parsing Redis RESP."
122-
)
113+
func parseInteger(_ buffer: inout ByteBuffer, _ position: inout Int) -> Int? {
114+
guard let stringBuffer = parseSimpleString(&buffer, &position) else { return nil }
115+
return stringBuffer.withUnsafeReadableBytes { ptr in
116+
Int(strtoll(ptr.bindMemory(to: Int8.self).baseAddress!, nil, 10))
123117
}
124-
125-
return number
126118
}
127119

128120
/// See [https://redis.io/topics/protocol#resp-bulk-strings](https://redis.io/topics/protocol#resp-bulk-strings)
129-
func _parseBulkString(at position: inout Int, from buffer: inout ByteBuffer) throws -> ParsingState {
130-
guard let size = try _parseInteger(at: &position, from: &buffer) else { return .notYetParsed }
121+
func parseBulkString(_ buffer: inout ByteBuffer, _ position: inout Int) -> ParsingState {
122+
guard let size = parseInteger(&buffer, &position) else { return .notYetParsed }
131123

132-
// Redis sends '-1' to represent a null string
124+
// Redis sends '$-1\r\n' to represent a null bulk string
133125
guard size > -1 else { return .parsed(.null) }
134126

135-
// verify that we have our expected bulk string message
136-
// by adding the expected CRLF bytes to the parsed size
137-
// even if the size is 0, Redis provides line endings (i.e. $0\r\n\r\n)
127+
// verify that we have the entire bulk string message by adding the expected CRLF bytes
128+
// to the parsed size of the message content
129+
// even if the content is empty, Redis send '$0\r\n\r\n'
138130
let readableByteCount = buffer.readableBytes - position
139131
let expectedRemainingMessageSize = size + 2
140132
guard readableByteCount >= expectedRemainingMessageSize else { return .notYetParsed }
141133

134+
// empty bulk strings, different from null strings, are represented as .bulkString(nil)
142135
guard size > 0 else {
143-
// Move the tip of the message position
136+
// move the parsing position to the newline for recursive parsing
144137
position += 2
145-
return .parsed(.bulkString([]))
138+
return .parsed(.bulkString(nil))
146139
}
147140

148-
guard let bytes = buffer.copyBytes(at: position, length: expectedRemainingMessageSize) else {
141+
guard let bytes = buffer.viewBytes(at: position, length: expectedRemainingMessageSize) else {
149142
return .notYetParsed
150143
}
151144

152-
// Move the tip of the message position for recursive parsing to just after the newline
153-
// of the bulk string content
145+
// move the parsing position to the newline for recursive parsing
154146
position += expectedRemainingMessageSize
155147

156148
return .parsed(.bulkString(
157-
.init(bytes[..<size])
149+
buffer.getSlice(at: bytes.startIndex, length: size)
158150
))
159151
}
160152

161153
/// See [https://redis.io/topics/protocol#resp-arrays](https://redis.io/topics/protocol#resp-arrays)
162-
func _parseArray(at position: inout Int, from buffer: inout ByteBuffer) throws -> ParsingState {
163-
guard let arraySize = try _parseInteger(at: &position, from: &buffer) else { return .notYetParsed }
164-
guard arraySize > -1 else { return .parsed(.null) }
165-
guard arraySize > 0 else { return .parsed(.array([])) }
166-
167-
var array = [ParsingState](repeating: .notYetParsed, count: arraySize)
168-
for index in 0..<arraySize {
169-
guard buffer.readableBytes - position > 0 else { return .notYetParsed }
170-
171-
let parseResult = try parse(at: &position, from: &buffer)
172-
switch parseResult {
173-
case .parsed:
174-
array[index] = parseResult
175-
default:
176-
return .notYetParsed
154+
func parseArray(_ buffer: inout ByteBuffer, _ position: inout Int) throws -> ParsingState {
155+
guard let elementCount = parseInteger(&buffer, &position) else { return .notYetParsed }
156+
guard elementCount > -1 else { return .parsed(.null) } // '*-1\r\n'
157+
guard elementCount > 0 else { return .parsed(.array([])) } // '*0\r\n'
158+
159+
var results = [ParsingState](repeating: .notYetParsed, count: elementCount)
160+
for index in 0..<elementCount {
161+
guard
162+
var slice = buffer.getSlice(at: position, length: buffer.readableBytes - position)
163+
else { return .notYetParsed }
164+
165+
var subPosition = 0
166+
let result = try parse(from: &slice, index: &subPosition)
167+
switch result {
168+
case .parsed: results[index] = result
169+
default: return .notYetParsed
177170
}
171+
172+
position += subPosition
178173
}
179174

180-
let values = try array.map { state -> RESPValue in
181-
guard case .parsed(let value) = state else {
182-
throw RedisError(
183-
identifier: "parseArray",
184-
reason: "Unexpected error while parsing Redis RESP."
185-
)
175+
let values = try results.map { state -> RESPValue in
176+
guard case let .parsed(value) = state else {
177+
throw RedisError(identifier: "parseArray", reason: "Unexpected error while parsing RESP.")
186178
}
187179
return value
188180
}
189-
return .parsed(.array(values))
181+
return .parsed(.array(ContiguousArray<RESPValue>(values)))
190182
}
191183
}

0 commit comments

Comments
 (0)