Skip to content

Commit 9225dc3

Browse files
Joannisfabianfett
andauthoredJul 5, 2023
Introduces a RESP3Token wrapper struct around ByteBuffer (#71)
Co-authored-by: Fabian Fett <fabianfett@apple.com>
1 parent 89a29d4 commit 9225dc3

File tree

5 files changed

+1077
-0
lines changed

5 files changed

+1077
-0
lines changed
 

‎Package.swift

+15
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ let package = Package(
5050
"RediStack"
5151
]
5252
),
53+
.target(
54+
name: "RESP3",
55+
dependencies: [
56+
.product(name: "NIOCore", package: "swift-nio"),
57+
]
58+
),
5359
.testTarget(
5460
name: "RediStackTests",
5561
dependencies: [
@@ -67,6 +73,15 @@ let package = Package(
6773
.product(name: "NIO", package: "swift-nio")
6874
]
6975
),
76+
.testTarget(
77+
name: "RESP3Tests",
78+
dependencies: [
79+
"RESP3",
80+
.product(name: "NIOCore", package: "swift-nio"),
81+
.product(name: "NIOEmbedded", package: "swift-nio"),
82+
.product(name: "NIOTestUtils", package: "swift-nio"),
83+
]
84+
),
7085
.testTarget(
7186
name: "RediStackIntegrationTests",
7287
dependencies: [

‎Sources/RESP3/RESP3Error.swift

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2023 RediStack 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 RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
17+
/// This error is thrown if a RESP3 package could not be decoded.
18+
///
19+
/// If you see this error, there a two potential reasons this might happen:
20+
///
21+
/// 1. The Swift RESP3 implementation is wrong
22+
/// 2. You are contacting an untrusted backend
23+
///
24+
struct RESP3ParsingError: Error {
25+
struct Code: Hashable, Sendable, CustomStringConvertible {
26+
private enum Base {
27+
case invalidLeadingByte
28+
case invalidData
29+
case tooDeeplyNestedAggregatedTypes
30+
case missingColonInVerbatimString
31+
case canNotParseInteger
32+
case canNotParseDouble
33+
case canNotParseBigNumber
34+
}
35+
36+
private let base: Base
37+
38+
private init(_ base: Base) {
39+
self.base = base
40+
}
41+
42+
static let invalidLeadingByte = Self.init(.invalidLeadingByte)
43+
static let invalidData = Self.init(.invalidData)
44+
static let tooDeeplyNestedAggregatedTypes = Self.init(.tooDeeplyNestedAggregatedTypes)
45+
static let missingColonInVerbatimString = Self.init(.missingColonInVerbatimString)
46+
static let canNotParseInteger = Self.init(.canNotParseInteger)
47+
static let canNotParseDouble = Self.init(.canNotParseDouble)
48+
static let canNotParseBigNumber = Self.init(.canNotParseBigNumber)
49+
50+
var description: String {
51+
switch self.base {
52+
case .invalidLeadingByte:
53+
return "invalidLeadingByte"
54+
case .invalidData:
55+
return "invalidData"
56+
case .tooDeeplyNestedAggregatedTypes:
57+
return "tooDeeplyNestedAggregatedTypes"
58+
case .missingColonInVerbatimString:
59+
return "missingColonInVerbatimString"
60+
case .canNotParseInteger:
61+
return "canNotParseInteger"
62+
case .canNotParseDouble:
63+
return "canNotParseDouble"
64+
case .canNotParseBigNumber:
65+
return "canNotParseBigNumber"
66+
}
67+
}
68+
}
69+
70+
var code: Code
71+
72+
var buffer: ByteBuffer
73+
}
74+
75+
enum RESP3Error: Error, Equatable {
76+
case dataMalformed
77+
case invalidType(UInt8)
78+
case tooDepplyNestedAggregatedTypes
79+
}

‎Sources/RESP3/RESP3Token.swift

+530
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,530 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2023 RediStack 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 RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
17+
public struct RESP3Token: Hashable, Sendable {
18+
public struct Array: Sequence, Sendable, Hashable {
19+
public typealias Element = RESP3Token
20+
21+
public let count: Int
22+
let buffer: ByteBuffer
23+
24+
public func makeIterator() -> Iterator {
25+
Iterator(buffer: self.buffer)
26+
}
27+
28+
public struct Iterator: IteratorProtocol {
29+
public typealias Element = RESP3Token
30+
31+
private var buffer: ByteBuffer
32+
33+
fileprivate init(buffer: ByteBuffer) {
34+
self.buffer = buffer
35+
}
36+
37+
public mutating func next() -> RESP3Token? {
38+
return try! RESP3Token(consuming: &self.buffer)
39+
}
40+
}
41+
}
42+
43+
public struct Map: Sequence, Sendable, Hashable {
44+
public typealias Element = (key: RESP3Token, value: RESP3Token)
45+
46+
public let count: Int
47+
let underlying: Array
48+
49+
init(count: Int, buffer: ByteBuffer) {
50+
self.count = count
51+
self.underlying = Array(count: count * 2, buffer: buffer)
52+
}
53+
54+
public func makeIterator() -> Iterator {
55+
Iterator(underlying: self.underlying.makeIterator())
56+
}
57+
58+
public struct Iterator: IteratorProtocol {
59+
public typealias Element = (key: RESP3Token, value: RESP3Token)
60+
61+
private var underlying: Array.Iterator
62+
63+
fileprivate init(underlying: Array.Iterator) {
64+
self.underlying = underlying
65+
}
66+
67+
public mutating func next() -> (key: RESP3Token, value: RESP3Token)? {
68+
guard let key = self.underlying.next() else {
69+
return nil
70+
}
71+
72+
let value = self.underlying.next()!
73+
return (key, value)
74+
}
75+
}
76+
}
77+
78+
public enum Value: Hashable {
79+
case simpleString(ByteBuffer)
80+
case simpleError(ByteBuffer)
81+
case blobString(ByteBuffer)
82+
case blobError(ByteBuffer)
83+
case verbatimString(ByteBuffer)
84+
case number(Int64)
85+
case double(Double)
86+
case boolean(Bool)
87+
case null
88+
case bigNumber(ByteBuffer)
89+
case array(Array)
90+
case attribute(Map)
91+
case map(Map)
92+
case set(Array)
93+
case push(Array)
94+
}
95+
96+
let base: ByteBuffer
97+
98+
public var value: Value {
99+
var local = self.base
100+
101+
switch local.readValidatedRESP3TypeIdentifier() {
102+
case .null:
103+
return .null
104+
105+
case .boolean:
106+
return .boolean(local.readInteger(as: UInt8.self)! == .t)
107+
108+
case .blobString:
109+
var lengthSlice = try! local.readCRLFTerminatedSlice2()!
110+
let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)!
111+
let length = Int(lengthString)!
112+
return .blobString(local.readSlice(length: length)!)
113+
114+
case .blobError:
115+
var lengthSlice = try! local.readCRLFTerminatedSlice2()!
116+
let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)!
117+
let length = Int(lengthString)!
118+
return .blobError(local.readSlice(length: length)!)
119+
120+
case .simpleString:
121+
let slice = try! local.readCRLFTerminatedSlice2()!
122+
return .simpleString(slice)
123+
124+
case .simpleError:
125+
let slice = try! local.readCRLFTerminatedSlice2()!
126+
return .simpleError(slice)
127+
128+
case .array:
129+
var countSlice = try! local.readCRLFTerminatedSlice2()!
130+
let countString = countSlice.readString(length: countSlice.readableBytes)!
131+
let count = Int(countString)!
132+
return .array(.init(count: count, buffer: local))
133+
134+
case .push:
135+
var countSlice = try! local.readCRLFTerminatedSlice2()!
136+
let countString = countSlice.readString(length: countSlice.readableBytes)!
137+
let count = Int(countString)!
138+
return .push(.init(count: count, buffer: local))
139+
140+
case .set:
141+
var countSlice = try! local.readCRLFTerminatedSlice2()!
142+
let countString = countSlice.readString(length: countSlice.readableBytes)!
143+
let count = Int(countString)!
144+
return .set(.init(count: count, buffer: local))
145+
146+
case .attribute:
147+
var countSlice = try! local.readCRLFTerminatedSlice2()!
148+
let countString = countSlice.readString(length: countSlice.readableBytes)!
149+
let count = Int(countString)!
150+
return .attribute(.init(count: count, buffer: local))
151+
152+
case .map:
153+
var countSlice = try! local.readCRLFTerminatedSlice2()!
154+
let countString = countSlice.readString(length: countSlice.readableBytes)!
155+
let count = Int(countString)!
156+
return .map(.init(count: count, buffer: local))
157+
158+
case .integer:
159+
var numberSlice = try! local.readCRLFTerminatedSlice2()!
160+
let numberString = numberSlice.readString(length: numberSlice.readableBytes)!
161+
let number = Int64(numberString)!
162+
return .number(number)
163+
164+
case .double:
165+
var numberSlice = try! local.readCRLFTerminatedSlice2()!
166+
let numberString = numberSlice.readString(length: numberSlice.readableBytes)!
167+
let number = Double(numberString)!
168+
return .double(number)
169+
170+
case .verbatimString:
171+
var lengthSlice = try! local.readCRLFTerminatedSlice2()!
172+
let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)!
173+
let length = Int(lengthString)!
174+
return .verbatimString(local.readSlice(length: length)!)
175+
176+
case .bigNumber:
177+
let lengthSlice = try! local.readCRLFTerminatedSlice2()!
178+
return .bigNumber(lengthSlice)
179+
}
180+
}
181+
182+
public init?(consuming buffer: inout ByteBuffer) throws {
183+
try self.init(consuming: &buffer, depth: 0)
184+
}
185+
186+
fileprivate init?(consuming buffer: inout ByteBuffer, depth: Int) throws {
187+
let validated: ByteBuffer?
188+
189+
switch try buffer.getRESP3TypeIdentifier(at: buffer.readerIndex) {
190+
case .some(.null):
191+
validated = try buffer.readRESPNullSlice()
192+
193+
case .some(.boolean):
194+
validated = try buffer.readRESPBooleanSlice()
195+
196+
case .some(.blobString),
197+
.some(.verbatimString),
198+
.some(.blobError):
199+
validated = try buffer.readRESPBlobStringSlice()
200+
201+
case .some(.simpleString),
202+
.some(.simpleError):
203+
validated = try buffer.readRESPSimpleStringSlice()
204+
205+
case .some(.array),
206+
.some(.push),
207+
.some(.set),
208+
.some(.map),
209+
.some(.attribute):
210+
validated = try buffer.readRESPAggregateSlice(depth: depth)
211+
212+
case .some(.integer):
213+
validated = try buffer.readRESPIntegerSlice()
214+
215+
case .some(.double):
216+
validated = try buffer.readRESPDoubleSlice()
217+
218+
case .some(.bigNumber):
219+
validated = try buffer.readRESPBigNumberSlice()
220+
221+
case .none:
222+
return nil
223+
}
224+
225+
guard let validated = validated else { return nil }
226+
self.base = validated
227+
}
228+
229+
init(validated: ByteBuffer) {
230+
self.base = validated
231+
}
232+
}
233+
234+
extension ByteBuffer {
235+
fileprivate mutating func getRESP3TypeIdentifier(at index: Int) throws -> RESP3TypeIdentifier? {
236+
guard let int = self.getInteger(at: index, as: UInt8.self) else {
237+
return nil
238+
}
239+
240+
guard let id = RESP3TypeIdentifier(rawValue: int) else {
241+
throw RESP3ParsingError(code: .invalidLeadingByte, buffer: self)
242+
}
243+
244+
return id
245+
}
246+
247+
fileprivate mutating func readValidatedRESP3TypeIdentifier() -> RESP3TypeIdentifier {
248+
let int = self.readInteger(as: UInt8.self)!
249+
return RESP3TypeIdentifier(rawValue: int)!
250+
}
251+
252+
fileprivate mutating func readRESPNullSlice() throws -> ByteBuffer? {
253+
let markerIndex = self.readerIndex
254+
let copy = self
255+
guard let (marker, crlf) = self.readMultipleIntegers(as: (UInt8, UInt16).self) else {
256+
return nil
257+
}
258+
259+
let resp3ID = RESP3TypeIdentifier(rawValue: marker)!
260+
precondition(resp3ID == .null)
261+
262+
if crlf == .crlf {
263+
return copy.getSlice(at: markerIndex, length: 3)!
264+
}
265+
266+
throw RESP3ParsingError(code: .invalidData, buffer: copy)
267+
}
268+
269+
fileprivate mutating func readRESPBooleanSlice() throws -> ByteBuffer? {
270+
var copy = self
271+
guard let resp = self.readInteger(as: UInt32.self) else {
272+
return nil
273+
}
274+
switch resp {
275+
case .respTrue:
276+
return copy.readSlice(length: 4)!
277+
case .respFalse:
278+
return copy.readSlice(length: 4)!
279+
default:
280+
throw RESP3ParsingError(code: .invalidData, buffer: copy)
281+
}
282+
}
283+
284+
fileprivate mutating func readRESPBlobStringSlice() throws -> ByteBuffer? {
285+
let marker = try self.getRESP3TypeIdentifier(at: self.readerIndex)!
286+
precondition(marker == .blobString || marker == .verbatimString || marker == .blobError)
287+
guard var lengthSlice = try self.getCRLFTerminatedSlice(at: self.readerIndex + 1) else {
288+
return nil
289+
}
290+
let lengthLineLength = lengthSlice.readableBytes + 2
291+
let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)!
292+
guard let blobLength = Int(lengthString) else {
293+
throw RESP3ParsingError(code: .canNotParseInteger, buffer: self)
294+
}
295+
296+
let respLength = 1 + lengthLineLength + blobLength + 2
297+
298+
guard let slice = self.readSlice(length: respLength) else {
299+
return nil
300+
}
301+
302+
// validate that the last two characters are \r\n
303+
if slice.getInteger(at: slice.readableBytes - 2, as: UInt16.self) != .crlf {
304+
throw RESP3ParsingError(code: .invalidData, buffer: slice)
305+
}
306+
307+
// validate that the fourth character is colon, if we have a verbatim string
308+
if marker == .verbatimString {
309+
let colonIndex = 1 + lengthLineLength + 3
310+
guard slice.readableBytes > colonIndex && slice.readableBytesView[colonIndex] == .colon else {
311+
throw RESP3ParsingError(code: .missingColonInVerbatimString, buffer: slice)
312+
}
313+
}
314+
315+
return slice
316+
}
317+
318+
fileprivate mutating func readRESPSimpleStringSlice() throws -> ByteBuffer? {
319+
let marker = try self.getRESP3TypeIdentifier(at: self.readerIndex)!
320+
precondition(marker == .simpleString || marker == .simpleError)
321+
guard let crIndex = try self.firstCRLFIndex(after: self.readerIndex + 1) else {
322+
return nil
323+
}
324+
325+
return self.readSlice(length: crIndex + 2 - self.readerIndex)
326+
}
327+
328+
fileprivate mutating func readRESPAggregateSlice(depth: Int) throws -> ByteBuffer? {
329+
let marker = try self.getRESP3TypeIdentifier(at: self.readerIndex)!
330+
guard depth < 1000 else {
331+
throw RESP3ParsingError(code: .tooDeeplyNestedAggregatedTypes, buffer: self)
332+
}
333+
334+
let multiplier: Int
335+
switch marker {
336+
case .array, .push, .set:
337+
multiplier = 1
338+
case .map, .attribute:
339+
multiplier = 2
340+
default:
341+
fatalError()
342+
}
343+
344+
guard var lengthSlice = try self.getCRLFTerminatedSlice(at: self.readerIndex + 1) else {
345+
return nil
346+
}
347+
let prefixLength = lengthSlice.readableBytes + 3
348+
let lengthString = lengthSlice.readString(length: lengthSlice.readableBytes)!
349+
guard let arrayLength = Int(lengthString) else {
350+
throw RESP3ParsingError(code: .canNotParseInteger, buffer: self)
351+
}
352+
353+
var localCopy = self
354+
localCopy.moveReaderIndex(forwardBy: prefixLength)
355+
356+
let elementCount = arrayLength * multiplier
357+
358+
func iterateChildren(consuming localCopy: inout ByteBuffer, count: Int, depth: Int) throws -> Int? {
359+
var bodyLength = 0
360+
for _ in 0..<elementCount {
361+
guard let new = try RESP3Token(consuming: &localCopy, depth: depth + 1) else {
362+
return nil
363+
}
364+
bodyLength += new.base.readableBytes
365+
}
366+
return bodyLength
367+
}
368+
369+
let bodyLength: Int?
370+
371+
if depth > 0 {
372+
bodyLength = try iterateChildren(consuming: &localCopy, count: elementCount, depth: depth)
373+
} else {
374+
do {
375+
bodyLength = try iterateChildren(consuming: &localCopy, count: elementCount, depth: depth)
376+
} catch var error as RESP3ParsingError {
377+
error.buffer = self
378+
throw error
379+
}
380+
}
381+
382+
guard let bodyLength = bodyLength else { return nil }
383+
384+
return self.readSlice(length: prefixLength + bodyLength)
385+
}
386+
387+
fileprivate mutating func readRESPIntegerSlice() throws -> ByteBuffer? {
388+
let marker = try self.getRESP3TypeIdentifier(at: self.readerIndex)!
389+
precondition(marker == .integer)
390+
391+
guard var slice = try self.getCRLFTerminatedSlice(at: self.readerIndex + 1) else {
392+
return nil
393+
}
394+
395+
let lineLength = slice.readableBytes + 3
396+
let string = slice.readString(length: slice.readableBytes)!
397+
if Int64(string) == nil {
398+
throw RESP3ParsingError(code: .canNotParseInteger, buffer: self)
399+
}
400+
401+
return self.readSlice(length: lineLength)!
402+
}
403+
404+
fileprivate mutating func readRESPDoubleSlice() throws -> ByteBuffer? {
405+
let marker = try self.getRESP3TypeIdentifier(at: self.readerIndex)!
406+
precondition(marker == .double)
407+
408+
guard var slice = try self.getCRLFTerminatedSlice(at: self.readerIndex + 1) else {
409+
return nil
410+
}
411+
412+
let lineLength = slice.readableBytes + 3
413+
let string = slice.readString(length: slice.readableBytes)!
414+
if Double(string) == nil {
415+
throw RESP3ParsingError(code: .canNotParseDouble, buffer: self)
416+
}
417+
418+
return self.readSlice(length: lineLength)!
419+
}
420+
421+
fileprivate mutating func readRESPBigNumberSlice() throws -> ByteBuffer? {
422+
let marker = try self.getRESP3TypeIdentifier(at: self.readerIndex)!
423+
precondition(marker == .bigNumber)
424+
425+
guard let slice = try self.getCRLFTerminatedSlice(at: self.readerIndex + 1) else {
426+
return nil
427+
}
428+
429+
var i = 0
430+
var negative = false
431+
for digit in slice.readableBytesView {
432+
defer { i += 1 }
433+
switch digit {
434+
case UInt8(ascii: "0")...UInt8(ascii: "9"):
435+
continue
436+
437+
case UInt8(ascii: "-") where i == 0:
438+
negative = true
439+
continue
440+
441+
default:
442+
throw RESP3ParsingError(code: .canNotParseBigNumber, buffer: self)
443+
}
444+
}
445+
446+
if slice.readableBytes == 0 || (negative && slice.readableBytes <= 1) {
447+
throw RESP3ParsingError(code: .canNotParseBigNumber, buffer: self)
448+
}
449+
450+
return self.readSlice(length: slice.readableBytes + 3)!
451+
}
452+
453+
fileprivate mutating func readCRLFTerminatedSlice2() throws -> ByteBuffer? {
454+
guard let slice = try self.getCRLFTerminatedSlice(at: self.readerIndex) else {
455+
return nil
456+
}
457+
458+
self.moveReaderIndex(forwardBy: slice.readableBytes + 2)
459+
return slice
460+
}
461+
462+
private func getCRLFTerminatedSlice(at index: Int) throws -> ByteBuffer? {
463+
guard let crIndex = try self.firstCRLFIndex(after: index) else {
464+
return nil
465+
}
466+
467+
return self.getSlice(at: index, length: crIndex - index)!
468+
}
469+
470+
private func firstCRLFIndex(after index: Int) throws -> Int? {
471+
if self.readableBytesView.isEmpty { return nil }
472+
guard let crIndex = self.readableBytesView[index...].firstIndex(where: { $0 == .carriageReturn }) else {
473+
return nil
474+
}
475+
476+
guard crIndex + 1 < self.readableBytesView.endIndex else {
477+
return nil
478+
}
479+
480+
guard self.getInteger(at: crIndex + 1, as: UInt8.self)! == .newline else {
481+
throw RESP3ParsingError(code: .invalidData, buffer: self)
482+
}
483+
484+
return crIndex
485+
}
486+
}
487+
488+
extension UInt16 {
489+
fileprivate static let crlf: UInt16 = {
490+
var value: UInt16 = 0
491+
let cr = UInt8.carriageReturn
492+
value += UInt16(UInt8.carriageReturn) << 8
493+
value += UInt16(UInt8.newline)
494+
return value
495+
}()
496+
}
497+
498+
extension UInt32 {
499+
fileprivate static let respTrue: UInt32 = {
500+
var value: UInt32 = 0
501+
value += UInt32(UInt8.pound) << 24
502+
value += UInt32(UInt8.t) << 16
503+
value += UInt32(UInt8.carriageReturn) << 8
504+
value += UInt32(UInt8.newline)
505+
return value
506+
}()
507+
508+
fileprivate static let respFalse: UInt32 = {
509+
var value: UInt32 = 0
510+
value += UInt32(UInt8.pound) << 24
511+
value += UInt32(UInt8.f) << 16
512+
value += UInt32(UInt8.carriageReturn) << 8
513+
value += UInt32(UInt8.newline)
514+
return value
515+
}()
516+
}
517+
518+
public struct RESP3TokenDecoder: NIOSingleStepByteToMessageDecoder {
519+
public typealias InboundOut = RESP3Token
520+
521+
public init() {}
522+
523+
public mutating func decode(buffer: inout ByteBuffer) throws -> RESP3Token? {
524+
try RESP3Token(consuming: &buffer)
525+
}
526+
527+
public mutating func decodeLast(buffer: inout ByteBuffer, seenEOF _: Bool) throws -> RESP3Token? {
528+
try self.decode(buffer: &buffer)
529+
}
530+
}
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2023 RediStack 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 RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
enum RESP3TypeIdentifier: UInt8 {
16+
case integer = 58 // UInt8(ascii: ":")
17+
case double = 44 // UInt8.comma
18+
case simpleString = 43 // UInt8.plus
19+
case simpleError = 45 // UInt8.min
20+
case blobString = 36 // UInt8.dollar
21+
case blobError = 33 // UInt8.exclamationMark
22+
case verbatimString = 61 // UInt8.equals
23+
case boolean = 35 // UInt8.pound
24+
case null = 95 // UInt8.underscore
25+
case bigNumber = 40 // UInt8.leftRoundBracket
26+
case array = 42 // UInt8.asterisk
27+
case map = 37 // UInt8.percent
28+
case set = 126 // UInt8.tilde
29+
case attribute = 124 // UInt8.pipe
30+
case push = 62 // UInt8.rightAngledBracket
31+
}
32+
33+
extension UInt8 {
34+
static let newline = UInt8(ascii: "\n")
35+
static let carriageReturn = UInt8(ascii: "\r")
36+
static let colon = UInt8(ascii: ":")
37+
static let pound = UInt8(ascii: "#")
38+
static let t = UInt8(ascii: "t")
39+
static let f = UInt8(ascii: "f")
40+
}
+413
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,413 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the RediStack open source project
4+
//
5+
// Copyright (c) 2023 RediStack 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 RediStack project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOTestUtils
17+
@testable import RESP3
18+
import XCTest
19+
20+
final class RESP3TokenTests: XCTestCase {
21+
func testRESPNullToken() {
22+
let input = ByteBuffer(string: "_\r\n")
23+
let respNull = RESP3Token(validated: input)
24+
25+
XCTAssertNoThrow(
26+
try ByteToMessageDecoderVerifier.verifyDecoder(
27+
inputOutputPairs: [(input, [respNull])],
28+
decoderFactory: { RESP3TokenDecoder() }
29+
)
30+
)
31+
32+
XCTAssertEqual(respNull.value, .null)
33+
}
34+
35+
func testRESPBool() {
36+
let inputTrue = ByteBuffer(string: "#t\r\n")
37+
let inputFalse = ByteBuffer(string: "#f\r\n")
38+
let respTrue = RESP3Token(validated: inputTrue)
39+
let respFalse = RESP3Token(validated: inputFalse)
40+
41+
XCTAssertNoThrow(
42+
try ByteToMessageDecoderVerifier.verifyDecoder(
43+
inputOutputPairs: [
44+
(inputTrue, [respTrue]),
45+
(inputFalse, [respFalse]),
46+
],
47+
decoderFactory: { RESP3TokenDecoder() }
48+
)
49+
)
50+
51+
XCTAssertEqual(respTrue.value, .boolean(true))
52+
XCTAssertEqual(respFalse.value, .boolean(false))
53+
}
54+
55+
func testRESPNumber() {
56+
let input123123 = ByteBuffer(string: ":123123\r\n")
57+
let input42 = ByteBuffer(string: ":42\r\n")
58+
let input0 = ByteBuffer(string: ":0\r\n")
59+
let inputMax = ByteBuffer(string: ":\(Int64.max)\r\n")
60+
let inputMin = ByteBuffer(string: ":\(Int64.min)\r\n")
61+
let resp123123 = RESP3Token(validated: input123123)
62+
let resp42 = RESP3Token(validated: input42)
63+
let resp0 = RESP3Token(validated: input0)
64+
let respMax = RESP3Token(validated: inputMax)
65+
let respMin = RESP3Token(validated: inputMin)
66+
67+
XCTAssertNoThrow(
68+
try ByteToMessageDecoderVerifier.verifyDecoder(
69+
inputOutputPairs: [
70+
(input123123, [resp123123]),
71+
(input42, [resp42]),
72+
(input0, [resp0]),
73+
(inputMax, [respMax]),
74+
(inputMin, [respMin]),
75+
],
76+
decoderFactory: { RESP3TokenDecoder() }
77+
)
78+
)
79+
80+
XCTAssertEqual(resp123123.value, .number(123_123))
81+
XCTAssertEqual(resp42.value, .number(42))
82+
XCTAssertEqual(resp0.value, .number(0))
83+
XCTAssertEqual(respMax.value, .number(.max))
84+
XCTAssertEqual(respMin.value, .number(.min))
85+
}
86+
87+
func testRESPNumberInvalid() {
88+
let invalid = [
89+
":\(Int.max)1\r\n",
90+
":\(Int.min)1\r\n",
91+
]
92+
93+
for value in invalid {
94+
let buffer = ByteBuffer(string: value)
95+
XCTAssertThrowsError(
96+
try ByteToMessageDecoderVerifier.verifyDecoder(
97+
inputOutputPairs: [
98+
(buffer, [RESP3Token(validated: .init())]),
99+
],
100+
decoderFactory: { RESP3TokenDecoder() }
101+
)
102+
) {
103+
guard let error = $0 as? RESP3ParsingError else { return XCTFail("Unexpected error: \($0)") }
104+
XCTAssertEqual(error.buffer, buffer)
105+
XCTAssertEqual(error.code, .canNotParseInteger)
106+
}
107+
}
108+
}
109+
110+
func testRESPDouble() {
111+
let input123 = ByteBuffer(string: ",1.23\r\n")
112+
let input42 = ByteBuffer(string: ",42\r\n")
113+
let input0 = ByteBuffer(string: ",0\r\n")
114+
let inputInf = ByteBuffer(string: ",inf\r\n")
115+
let inputNegInf = ByteBuffer(string: ",-inf\r\n")
116+
let inputNan = ByteBuffer(string: ",nan\r\n")
117+
let inputPi = ByteBuffer(string: ",\(Double.pi)\r\n")
118+
let inputExponent = ByteBuffer(string: ",1.4E12\r\n")
119+
let inputLowerExponent = ByteBuffer(string: ",1.4e-12\r\n")
120+
let resp123 = RESP3Token(validated: input123)
121+
let resp42 = RESP3Token(validated: input42)
122+
let resp0 = RESP3Token(validated: input0)
123+
let respInf = RESP3Token(validated: inputInf)
124+
let respNegInf = RESP3Token(validated: inputNegInf)
125+
let respNan = RESP3Token(validated: inputNan)
126+
let respPi = RESP3Token(validated: inputPi)
127+
let respExponent = RESP3Token(validated: inputExponent)
128+
let respLowerExponent = RESP3Token(validated: inputLowerExponent)
129+
130+
XCTAssertNoThrow(
131+
try ByteToMessageDecoderVerifier.verifyDecoder(
132+
inputOutputPairs: [
133+
(input123, [resp123]),
134+
(input42, [resp42]),
135+
(input0, [resp0]),
136+
(inputInf, [respInf]),
137+
(inputNegInf, [respNegInf]),
138+
(inputNan, [respNan]),
139+
(inputPi, [respPi]),
140+
(inputExponent, [respExponent]),
141+
(inputLowerExponent, [respLowerExponent]),
142+
],
143+
decoderFactory: { RESP3TokenDecoder() }
144+
)
145+
)
146+
147+
XCTAssertEqual(resp123.value, .double(1.23))
148+
XCTAssertEqual(resp42.value, .double(42))
149+
XCTAssertEqual(resp0.value, .double(0))
150+
XCTAssertEqual(respInf.value, .double(.infinity))
151+
XCTAssertEqual(respNegInf.value, .double(-.infinity))
152+
guard case .double(let value) = respNan.value else { return XCTFail("Expected a double") }
153+
XCTAssert(value.isNaN)
154+
XCTAssertEqual(respPi.value, .double(.pi))
155+
}
156+
157+
#if false
158+
// TODO: this test currently succeeds, even though it has an invalid value
159+
func testRESPDoubleInvalid() throws {
160+
let invalid = [
161+
",.1\r\n",
162+
]
163+
164+
for value in invalid {
165+
XCTAssertThrowsError(
166+
try ByteToMessageDecoderVerifier.verifyDecoder(
167+
inputOutputPairs: [
168+
(.init(string: value), [RESP3Token(validated: .init())]),
169+
],
170+
decoderFactory: { RESP3TokenDecoder() }
171+
)
172+
) {
173+
XCTAssertEqual($0 as? RESP3Error, .dataMalformed, "unexpected error: \($0)")
174+
}
175+
}
176+
}
177+
#endif
178+
179+
func testRESPBigNumber() {
180+
let valid = [
181+
"123",
182+
]
183+
184+
for value in valid {
185+
let tokenString = "(\(value)\r\n"
186+
let token = ByteBuffer(string: tokenString)
187+
XCTAssertNoThrow(
188+
try ByteToMessageDecoderVerifier.verifyDecoder(
189+
inputOutputPairs: [
190+
(token, [RESP3Token(validated: token)]),
191+
],
192+
decoderFactory: { RESP3TokenDecoder() }
193+
),
194+
"Unexpected error for input: \(String(reflecting: tokenString))"
195+
)
196+
197+
XCTAssertEqual(RESP3Token(validated: token).value, .bigNumber(.init(string: value)))
198+
}
199+
}
200+
201+
func testRESPBigNumberInvalid() {
202+
let invalid = [
203+
"(--123\r\n",
204+
"(12-12\r\n",
205+
"(-\r\n",
206+
"(\r\n",
207+
]
208+
209+
for value in invalid {
210+
let buffer = ByteBuffer(string: value)
211+
XCTAssertThrowsError(
212+
try ByteToMessageDecoderVerifier.verifyDecoder(
213+
inputOutputPairs: [
214+
(buffer, [RESP3Token(validated: .init())]),
215+
],
216+
decoderFactory: { RESP3TokenDecoder() }
217+
)
218+
) {
219+
guard let error = $0 as? RESP3ParsingError else { return XCTFail("Unexpected error: \($0)") }
220+
XCTAssertEqual(error.buffer, buffer)
221+
XCTAssertEqual(error.code, .canNotParseBigNumber)
222+
}
223+
}
224+
}
225+
226+
func testBlobString() {
227+
let inputString = ByteBuffer(string: "$12\r\naaaabbbbcccc\r\n")
228+
let respString = RESP3Token(validated: inputString)
229+
230+
let inputError = ByteBuffer(string: "!21\r\nSYNTAX invalid syntax\r\n")
231+
let respError = RESP3Token(validated: inputError)
232+
233+
let inputVerbatim = ByteBuffer(string: "=16\r\ntxt:aaaabbbbcccc\r\n")
234+
let respVerbatim = RESP3Token(validated: inputVerbatim)
235+
236+
XCTAssertNoThrow(
237+
try ByteToMessageDecoderVerifier.verifyDecoder(
238+
inputOutputPairs: [
239+
(inputString, [respString]),
240+
(inputError, [respError]),
241+
(inputString, [respString]),
242+
],
243+
decoderFactory: { RESP3TokenDecoder() }
244+
)
245+
)
246+
247+
XCTAssertEqual(respString.value, .blobString(ByteBuffer(string: "aaaabbbbcccc")))
248+
XCTAssertEqual(respError.value, .blobError(ByteBuffer(string: "SYNTAX invalid syntax")))
249+
XCTAssertEqual(respVerbatim.value, .verbatimString(ByteBuffer(string: "txt:aaaabbbbcccc")))
250+
}
251+
252+
func testSimpleString() {
253+
let inputString = ByteBuffer(string: "+aaaabbbbcccc\r\n")
254+
let respString = RESP3Token(validated: inputString)
255+
let inputError = ByteBuffer(string: "-eeeeffffgggg\r\n")
256+
let respError = RESP3Token(validated: inputError)
257+
258+
XCTAssertNoThrow(
259+
try ByteToMessageDecoderVerifier.verifyDecoder(
260+
inputOutputPairs: [
261+
(inputString, [respString]),
262+
(inputError, [respError]),
263+
],
264+
decoderFactory: { RESP3TokenDecoder() }
265+
)
266+
)
267+
268+
XCTAssertEqual(respString.value, .simpleString(ByteBuffer(string: "aaaabbbbcccc")))
269+
XCTAssertEqual(respError.value, .simpleError(ByteBuffer(string: "eeeeffffgggg")))
270+
}
271+
272+
func testArray() {
273+
let emptyArrayInput = ByteBuffer(string: "*0\r\n")
274+
let respEmptyArray = RESP3Token(validated: emptyArrayInput)
275+
276+
let simpleStringArray1Input = ByteBuffer(string: "*1\r\n+aaaabbbbcccc\r\n")
277+
let respSimpleStringArray1 = RESP3Token(validated: simpleStringArray1Input)
278+
279+
let simpleStringArray2Input = ByteBuffer(string: "*2\r\n+aaaa\r\n+bbbb\r\n")
280+
let respSimpleStringArray2 = RESP3Token(validated: simpleStringArray2Input)
281+
282+
let simpleStringArray3Input = ByteBuffer(string: "*3\r\n*0\r\n+a\r\n-b\r\n")
283+
let respSimpleStringArray3 = RESP3Token(validated: simpleStringArray3Input)
284+
285+
let simpleStringPush3Input = ByteBuffer(string: ">3\r\n*0\r\n+a\r\n-b\r\n")
286+
let respSimpleStringPush3 = RESP3Token(validated: simpleStringPush3Input)
287+
288+
let simpleStringSet3Input = ByteBuffer(string: "~3\r\n*0\r\n+a\r\n#t\r\n")
289+
let respSimpleStringSet3 = RESP3Token(validated: simpleStringSet3Input)
290+
291+
XCTAssertNoThrow(
292+
try ByteToMessageDecoderVerifier.verifyDecoder(
293+
inputOutputPairs: [
294+
(emptyArrayInput, [respEmptyArray]),
295+
(simpleStringArray1Input, [respSimpleStringArray1]),
296+
(simpleStringArray2Input, [respSimpleStringArray2]),
297+
(simpleStringArray3Input, [respSimpleStringArray3]),
298+
(simpleStringPush3Input, [respSimpleStringPush3]),
299+
(simpleStringSet3Input, [respSimpleStringSet3]),
300+
],
301+
decoderFactory: { RESP3TokenDecoder() }
302+
)
303+
)
304+
305+
XCTAssertEqual(respEmptyArray.value, .array(.init(count: 0, buffer: .init())))
306+
XCTAssertEqual(respSimpleStringArray1.value, .array(.init(count: 1, buffer: .init(string: "+aaaabbbbcccc\r\n"))))
307+
XCTAssertEqual(respSimpleStringArray2.value, .array(.init(count: 2, buffer: .init(string: "+aaaa\r\n+bbbb\r\n"))))
308+
XCTAssertEqual(respSimpleStringArray3.value, .array(.init(count: 3, buffer: .init(string: "*0\r\n+a\r\n-b\r\n"))))
309+
XCTAssertEqual(respSimpleStringPush3.value, .push(.init(count: 3, buffer: .init(string: "*0\r\n+a\r\n-b\r\n"))))
310+
XCTAssertEqual(respSimpleStringSet3.value, .set(.init(count: 3, buffer: .init(string: "*0\r\n+a\r\n#t\r\n"))))
311+
312+
XCTAssertEqual(respEmptyArray.testArray, [])
313+
XCTAssertEqual(respSimpleStringArray1.testArray, [.simpleString(.init(string: "aaaabbbbcccc"))])
314+
XCTAssertEqual(respSimpleStringArray2.testArray, [.simpleString(.init(string: "aaaa")), .simpleString(.init(string: "bbbb"))])
315+
XCTAssertEqual(respSimpleStringArray3.testArray, [.array(.init(count: 0, buffer: .init())), .simpleString(.init(string: "a")), .simpleError(.init(string: "b"))])
316+
XCTAssertEqual(respSimpleStringPush3.testArray, [.array(.init(count: 0, buffer: .init())), .simpleString(.init(string: "a")), .simpleError(.init(string: "b"))])
317+
XCTAssertEqual(respSimpleStringSet3.testArray, [.array(.init(count: 0, buffer: .init())), .simpleString(.init(string: "a")), .boolean(true)])
318+
}
319+
320+
func testDeeplyNestedRESPCantStackOverflow() {
321+
let pattern = [
322+
("*1\r\n", "*0\r\n"),
323+
(">1\r\n", ">0\r\n"),
324+
("~1\r\n", "~0\r\n"),
325+
("%1\r\n#t\r\n", "%0\r\n"),
326+
("|1\r\n#t\r\n", "|0\r\n")
327+
]
328+
329+
for (nested, final) in pattern {
330+
let tooDeeplyNested = String(repeating: nested, count: 1000) + final
331+
var tooDeeplyNestedBuffer = ByteBuffer(string: tooDeeplyNested)
332+
333+
let notDepplyEnoughToThrow = String(repeating: nested, count: 999) + final
334+
var notDepplyEnoughToThrowBuffer = ByteBuffer(string: notDepplyEnoughToThrow)
335+
let notDepplyEnoughToThrowExpected = RESP3Token(validated: notDepplyEnoughToThrowBuffer)
336+
337+
#if true
338+
XCTAssertThrowsError(try RESP3Token(consuming: &tooDeeplyNestedBuffer)) {
339+
guard let error = $0 as? RESP3ParsingError else { return XCTFail("Unexpected error: \($0)") }
340+
XCTAssertEqual(error.buffer, tooDeeplyNestedBuffer)
341+
XCTAssertEqual(error.code, .tooDeeplyNestedAggregatedTypes)
342+
}
343+
344+
XCTAssertEqual(try RESP3Token(consuming: &notDepplyEnoughToThrowBuffer), notDepplyEnoughToThrowExpected)
345+
#else
346+
// this is very slow right now. Once we have faster decoding we should use this instead.
347+
XCTAssertNoThrow(
348+
try ByteToMessageDecoderVerifier.verifyDecoder(
349+
inputOutputPairs: [
350+
(buffer, [expected]),
351+
],
352+
decoderFactory: { RESP3TokenDecoder() }
353+
)
354+
)
355+
#endif
356+
}
357+
}
358+
359+
func testMap() {
360+
let emptyMapInput = ByteBuffer(string: "%0\r\n")
361+
let respEmptyMap = RESP3Token(validated: emptyMapInput)
362+
363+
let simpleStringMap1Input = ByteBuffer(string: "%1\r\n+aaaa\r\n+bbbb\r\n")
364+
let respSimpleStringMap1 = RESP3Token(validated: simpleStringMap1Input)
365+
366+
let simpleStringAttributes1Input = ByteBuffer(string: "|1\r\n+aaaa\r\n#f\r\n")
367+
let respSimpleStringAttributes1 = RESP3Token(validated: simpleStringAttributes1Input)
368+
369+
XCTAssertNoThrow(
370+
try ByteToMessageDecoderVerifier.verifyDecoder(
371+
inputOutputPairs: [
372+
(emptyMapInput, [respEmptyMap]),
373+
(simpleStringMap1Input, [respSimpleStringMap1]),
374+
(simpleStringAttributes1Input, [respSimpleStringAttributes1]),
375+
],
376+
decoderFactory: { RESP3TokenDecoder() }
377+
)
378+
)
379+
380+
XCTAssertEqual(respEmptyMap.value, .map(.init(count: 0, buffer: .init())))
381+
XCTAssertEqual(respSimpleStringMap1.value, .map(.init(count: 1, buffer: .init(string: "+aaaa\r\n+bbbb\r\n"))))
382+
XCTAssertEqual(respSimpleStringAttributes1.value, .attribute(.init(count: 1, buffer: .init(string: "+aaaa\r\n#f\r\n"))))
383+
384+
XCTAssertEqual(respEmptyMap.testDict, [:])
385+
XCTAssertEqual(respSimpleStringMap1.testDict, [.simpleString(.init(string: "aaaa")): .simpleString(.init(string: "bbbb"))])
386+
XCTAssertEqual(respSimpleStringAttributes1.testDict, [.simpleString(.init(string: "aaaa")): .boolean(false)])
387+
}
388+
}
389+
390+
extension RESP3Token {
391+
var testArray: [RESP3Token.Value]? {
392+
switch value {
393+
case .array(let array), .push(let array), .set(let array):
394+
return [RESP3Token.Value](array.map { $0.value })
395+
default:
396+
return nil
397+
}
398+
}
399+
400+
var testDict: [RESP3Token.Value: RESP3Token.Value]? {
401+
switch value {
402+
case .map(let values), .attribute(let values):
403+
var result = [RESP3Token.Value: RESP3Token.Value]()
404+
result.reserveCapacity(values.count)
405+
for (key, value) in values {
406+
result[key.value] = value.value
407+
}
408+
return result
409+
default:
410+
return nil
411+
}
412+
}
413+
}

0 commit comments

Comments
 (0)
Please sign in to comment.