GitLab's annual major release is around the corner. Along with a lot of new and exciting features, there will be a few breaking changes. Learn more here.

RESPTranslator.swift 10.6 KB
Newer Older
1 2
//===----------------------------------------------------------------------===//
//
3
// This source file is part of the RediStack open source project
4
//
5
// Copyright (c) 2019 RediStack project authors
6 7 8
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
9
// See CONTRIBUTORS.txt for the list of RediStack project authors
10 11 12 13 14
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

Nathan Harris's avatar
Nathan Harris committed
15
import protocol Foundation.LocalizedError
16 17
import NIO

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
/// A helper object for translating between raw bytes and Swift types according to the Redis Serialization Protocol (RESP).
///
/// See [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
public struct RESPTranslator {
    public init() { }
}

// MARK: Writing RESP

/// The carriage return and newline escape symbols, used as the standard signal in RESP for a "message" end.
/// A "message" in this case is a single data type.
fileprivate let respEnd: StaticString = "\r\n"

extension ByteBuffer {
    /// Writes the `RESPValue` into the current buffer, following the RESP specification.
    ///
    /// See [https://redis.io/topics/protocol](https://redis.io/topics/protocol)
    /// - Parameter value: The value to write to the buffer.
    public mutating func writeRESPValue(_ value: RESPValue) {
        switch value {
        case .simpleString(var buffer):
            self.writeStaticString("+")
            self.writeBuffer(&buffer)
            self.writeStaticString(respEnd)
            
        case .bulkString(.some(var buffer)):
            self.writeStaticString("$")
            self.writeString("\(buffer.readableBytes)")
            self.writeStaticString(respEnd)
            self.writeBuffer(&buffer)
            self.writeStaticString(respEnd)
            
        case .bulkString(.none):
            self.writeStaticString("$0\r\n\r\n")
            
        case .integer(let number):
            self.writeStaticString(":")
            self.writeString(number.description)
            self.writeStaticString(respEnd)
            
        case .null:
            self.writeStaticString("$-1\r\n")
            
        case .error(let error):
            self.writeStaticString("-")
            self.writeString(error.message)
            self.writeStaticString(respEnd)
            
        case .array(let array):
            self.writeStaticString("*")
            self.writeString("\(array.count)")
            self.writeStaticString(respEnd)
            array.forEach { self.writeRESPValue($0) }
        }
    }
}

extension RESPTranslator {
    /// Writes the value into the desired `ByteBuffer` in RESP format.
    /// - Parameters:
    ///     - value: The value to write into the buffer.
    ///     - out: The `ByteBuffer` that should be written to.
    public func write<Value: RESPValueConvertible>(_ value: Value, into out: inout ByteBuffer) {
        out.writeRESPValue(value.convertedToRESPValue())
    }
}

// MARK: Reading RESP

87
extension UInt8 {
88 89 90 91 92 93 94
    static let newline = UInt8(ascii: "\n")
    static let carriageReturn = UInt8(ascii: "\r")
    static let dollar = UInt8(ascii: "$")
    static let asterisk = UInt8(ascii: "*")
    static let plus = UInt8(ascii: "+")
    static let hyphen = UInt8(ascii: "-")
    static let colon = UInt8(ascii: ":")
95 96
}

97
extension RESPTranslator {
98
    /// Possible errors thrown while parsing RESP messages.
99 100
    /// - Important: Any of these errors should be considered a **BUG**.
    ///
101 102 103 104 105 106 107 108 109 110
    /// Please file a bug at [https://www.gitlab.com/mordil/RediStack/-/issues](https://www.gitlab.com/mordil/RediStack/-/issues).
    public struct ParsingError: LocalizedError, Equatable {
        /// An invalid RESP data type identifier was found.
        public static let invalidToken = ParsingError(.invalidToken)
        /// A bulk string size did not match the RESP schema.
        public static let invalidBulkStringSize = ParsingError(.invalidBulkStringSize)
        /// A bulk string's declared size did not match its content size.
        public static let bulkStringSizeMismatch = ParsingError(.bulkStringSizeMismatch)
        /// A RESP integer did not follow the RESP schema.
        public static let invalidIntegerFormat = ParsingError(.invalidIntegerFormat)
111
        
Nathan Harris's avatar
Nathan Harris committed
112
        public var errorDescription: String? {
113 114 115 116 117 118 119 120 121 122 123
            return self.base.rawValue
        }
        
        private let base: Base
        private init(_ base: Base) { self.base = base }
        
        private enum Base: String, Equatable {
            case invalidToken = "Cannot parse RESP: invalid token"
            case invalidBulkStringSize = "Cannot parse RESP Bulk String: received invalid size"
            case bulkStringSizeMismatch = "Cannot parse RESP Bulk String: declared size and content size do not match"
            case invalidIntegerFormat = "Cannot parse RESP Integer: invalid integer format"
Nathan Harris's avatar
Nathan Harris committed
124 125
        }
    }
126 127 128
}

extension RESPTranslator {
129 130 131 132 133 134 135 136
    /// Attempts to parse a `RESPValue` from the `ByteBuffer`.
    /// - Important: The provided `buffer` will have its reader index moved on a successful parse.
    /// - Throws:
    ///     - `RESPTranslator.ParsingError.invalidToken` if the first byte is not an expected RESP Data Type token.
    /// - Parameter buffer: The buffer that contains the bytes that need to be parsed.
    /// - Returns: The parsed `RESPValue` or nil.
    public func parseBytes(from buffer: inout ByteBuffer) throws -> RESPValue? {
        var copy = buffer
Lukasa's avatar
Lukasa committed
137 138

        guard let token = copy.readInteger(as: UInt8.self) else { return nil }
139 140
        
        let result: RESPValue?
141 142
        switch token {
        case .plus:
143 144 145
            guard let value = self.parseSimpleString(from: &copy) else { return nil }
            result = .simpleString(value)
            
Nathan Harris's avatar
Nathan Harris committed
146
        case .colon:
Lukasa's avatar
Lukasa committed
147
            guard let value = try self.parseInteger(from: &copy) else { return nil }
148 149
            result = .integer(value)
            
Nathan Harris's avatar
Nathan Harris committed
150
        case .dollar:
151
            result = try self.parseBulkString(from: &copy)
152 153
            break
            
Nathan Harris's avatar
Nathan Harris committed
154
        case .asterisk:
155 156 157
            result = try self.parseArray(from: &copy)
            break
            
158
        case .hyphen:
159
            guard
160
                let stringBuffer = self.parseSimpleString(from: &copy),
161
                let message = stringBuffer.getString(at: 0, length: stringBuffer.readableBytes)
162 163 164
            else { return nil }
            result = .error(RedisError(reason: message))
            
165
        default: throw ParsingError.invalidToken
166
        }
167 168 169 170 171 172 173
        
        // if we successfully parsed a value, we need to update the original buffer's readerIndex
        if result != nil {
            buffer.moveReaderIndex(to: copy.readerIndex)
        }
        
        return result
174
    }
175
    
176
    /// See [https://redis.io/topics/protocol#resp-simple-strings](https://redis.io/topics/protocol#resp-simple-strings)
177 178
    internal func parseSimpleString(from buffer: inout ByteBuffer) -> ByteBuffer? {
        let bytes = buffer.readableBytesView
179
        guard
180
            let newlineIndex = bytes.firstIndex(of: .newline),
Lukasa's avatar
Lukasa committed
181
            newlineIndex - bytes.startIndex >= 1 // strings should at least have a CRLF ending
182
        else { return nil }
183 184 185 186 187 188 189 190 191 192
        
        // grab the bytes that we've determined is the full simple string,
        // and make sure to move the reader index afterwards
        defer {
            buffer.moveReaderIndex(to: newlineIndex + 1)
        }
        // the length of the string will be the position (delta'd by the start index) - 1,
        // as the last character is just before the position of the newline escape
        let endIndex = newlineIndex - bytes.startIndex
        return buffer.getSlice(at: bytes.startIndex, length: endIndex - 1)
193
    }
194
    
Nathan Harris's avatar
Nathan Harris committed
195
    /// See [https://redis.io/topics/protocol#resp-integers](https://redis.io/topics/protocol#resp-integers)
Lukasa's avatar
Lukasa committed
196
    internal func parseInteger(from buffer: inout ByteBuffer) throws -> Int? {
197 198 199 200
        guard
            var stringBuffer = parseSimpleString(from: &buffer),
            let string = stringBuffer.readString(length: stringBuffer.readableBytes)
        else { return nil }
Lukasa's avatar
Lukasa committed
201 202 203

        guard let result = Int(string) else { throw ParsingError.invalidIntegerFormat }
        return result
Nathan Harris's avatar
Nathan Harris committed
204
    }
205
    
Nathan Harris's avatar
Nathan Harris committed
206
    /// See [https://redis.io/topics/protocol#resp-bulk-strings](https://redis.io/topics/protocol#resp-bulk-strings)
207
    internal func parseBulkString(from buffer: inout ByteBuffer) throws -> RESPValue? {
Lukasa's avatar
Lukasa committed
208
        guard let size = try self.parseInteger(from: &buffer) else {
209 210 211 212 213
            return nil
        }
        
        // only -1 is the only valid negative value for a size
        guard size >= -1 else { throw ParsingError.invalidBulkStringSize }
214
        
215
        // Redis sends '$-1\r\n' to represent a null bulk string
216 217 218 219 220
        guard size > -1 else { return .null }
        
        // Verify that we have the entire bulk string message by adding the expected CRLF end bytes
        // to the parsed size of the message content.
        // Even if the content is empty, Redis sends '$0\r\n\r\n'
Nathan Harris's avatar
Nathan Harris committed
221
        let expectedRemainingMessageSize = size + 2
222 223
        guard buffer.readableBytes >= expectedRemainingMessageSize else { return nil }
        
224 225
        // sanity check that the declared content size matches the actual size.
        guard
Lukasa's avatar
Lukasa committed
226
            buffer.getInteger(at: buffer.readerIndex + expectedRemainingMessageSize - 1, as: UInt8.self) == .newline
227
        else { throw ParsingError.bulkStringSizeMismatch }
228 229
        
        // empty content bulk strings are different from null, and represented as .bulkString(nil)
Nathan Harris's avatar
Nathan Harris committed
230
        guard size > 0 else {
231 232
            buffer.moveReaderIndex(forwardBy: 2)
            return .bulkString(nil)
Nathan Harris's avatar
Nathan Harris committed
233
        }
234 235 236 237
        
        // move the reader position forward by the size of the total message (including the CRLF ending)
        defer {
            buffer.moveReaderIndex(forwardBy: expectedRemainingMessageSize)
Nathan Harris's avatar
Nathan Harris committed
238
        }
239 240 241 242
        
        return .bulkString(
            buffer.getSlice(at: buffer.readerIndex, length: size)
        )
Nathan Harris's avatar
Nathan Harris committed
243
    }
244
    
Nathan Harris's avatar
Nathan Harris committed
245
    /// See [https://redis.io/topics/protocol#resp-arrays](https://redis.io/topics/protocol#resp-arrays)
246
    internal func parseArray(from buffer: inout ByteBuffer) throws -> RESPValue? {
Lukasa's avatar
Lukasa committed
247
        guard let elementCount = try parseInteger(from: &buffer) else { return nil }
248 249 250
        guard elementCount > -1 else { return .null } // '*-1\r\n'
        guard elementCount > 0 else { return .array([]) } // '*0\r\n'
        
251
        var results: [RESPValue] = []
252 253 254 255 256 257
        results.reserveCapacity(elementCount)
        
        for _ in 0..<elementCount {
            guard buffer.readableBytes > 0 else { return nil }
            guard let element = try self.parseBytes(from: &buffer) else { return nil }
            results.append(element)
258
        }
259 260
        
        return .array(results)
261 262
    }
}