@@ -10,21 +10,6 @@ extension UInt8 {
10
10
static let colon : UInt8 = 0x3A
11
11
}
12
12
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
-
28
13
/// Handles incoming byte messages from Redis and decodes them according to the RESP protocol.
29
14
///
30
15
/// See: [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
@@ -42,150 +27,157 @@ public final class RESPDecoder {
42
27
///
43
28
/// See [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
44
29
/// - 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 }
49
38
50
39
position += 1
51
40
52
41
switch token {
53
42
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 ) )
56
45
57
46
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 ) )
60
49
61
50
case . dollar:
62
- return try _parseBulkString ( at : & position , from : & buffer )
51
+ return parseBulkString ( & slice , & position )
63
52
64
53
case . asterisk:
65
- return try _parseArray ( at : & position , from : & buffer )
54
+ return try parseArray ( & slice , & position )
66
55
67
56
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. " )
76
64
}
77
65
}
78
66
}
79
67
80
- // Parsing
68
+ extension RESPDecoder : ByteToMessageDecoder {
69
+ /// `ByteToMessageDecoder.InboundOut`
70
+ public typealias InboundOut = RESPValue
81
71
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
90
75
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
99
82
}
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
+ }
100
92
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 ? {
103
98
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
106
102
else { return nil }
107
103
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
110
106
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)
112
110
}
113
111
114
112
/// 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 ) )
123
117
}
124
-
125
- return number
126
118
}
127
119
128
120
/// 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 }
131
123
132
- // Redis sends '-1 ' to represent a null string
124
+ // Redis sends '$-1\r\n ' to represent a null bulk string
133
125
guard size > - 1 else { return . parsed( . null) }
134
126
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'
138
130
let readableByteCount = buffer. readableBytes - position
139
131
let expectedRemainingMessageSize = size + 2
140
132
guard readableByteCount >= expectedRemainingMessageSize else { return . notYetParsed }
141
133
134
+ // empty bulk strings, different from null strings, are represented as .bulkString(nil)
142
135
guard size > 0 else {
143
- // Move the tip of the message position
136
+ // move the parsing position to the newline for recursive parsing
144
137
position += 2
145
- return . parsed( . bulkString( [ ] ) )
138
+ return . parsed( . bulkString( nil ) )
146
139
}
147
140
148
- guard let bytes = buffer. copyBytes ( at: position, length: expectedRemainingMessageSize) else {
141
+ guard let bytes = buffer. viewBytes ( at: position, length: expectedRemainingMessageSize) else {
149
142
return . notYetParsed
150
143
}
151
144
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
154
146
position += expectedRemainingMessageSize
155
147
156
148
return . parsed( . bulkString(
157
- . init ( bytes [ ..< size] )
149
+ buffer . getSlice ( at : bytes. startIndex , length : size)
158
150
) )
159
151
}
160
152
161
153
/// 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
177
170
}
171
+
172
+ position += subPosition
178
173
}
179
174
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. " )
186
178
}
187
179
return value
188
180
}
189
- return . parsed( . array( values) )
181
+ return . parsed( . array( ContiguousArray < RESPValue > ( values) ) )
190
182
}
191
183
}
0 commit comments