├── .gitattributes ├── .gitignore ├── .travis.yml ├── Makefile ├── README.markdown ├── ddebug.h ├── redis-parser.c └── t ├── RedisParser.pm ├── pipeline.t ├── sanity.t └── version.t /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=Text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.so 3 | *.lo 4 | *~ 5 | reindex 6 | test_case.lua 7 | tags 8 | *.html 9 | *.o 10 | *.tar.gz 11 | *.zip 12 | *.mobi 13 | genmobi.sh 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: focal 3 | 4 | branches: 5 | only: 6 | - "master" 7 | 8 | os: linux 9 | 10 | language: c 11 | 12 | compiler: 13 | - gcc 14 | 15 | env: 16 | matrix: 17 | - LUA=1 LUA_INCLUDE_DIR=/usr/include/lua5.1 LUA_CMODULE_DIR=/usr/lib/lua/5.1 LUALIB=-llua5.1 18 | - LUAJIT=1 LUAJIT_PREFIX=/usr/local LUA_INCLUDE_DIR=/usr/local/include/luajit-2.1 LUA_CMODULE_DIR=/usr/lib/lua/5.1 LUALIB=-lluajit-5.1 19 | 20 | before_install: 21 | - git clone https://github.com/openresty/lua-cjson.git 22 | 23 | install: 24 | - > 25 | if [ -n "$LUAJIT" ]; then 26 | git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git ../luajit2 27 | pushd ../luajit2 || exit 1 28 | make -j$JOBS CCDEBUG=-g Q= PREFIX=$LUAJIT_PREFIX CC=$CC XCFLAGS='-DLUA_USE_APICHECK -DLUA_USE_ASSERT' 29 | sudo make install > build.log 2>&1 || (cat build.log && exit 1) 30 | popd || exit 1 31 | fi 32 | - > 33 | if [ -n "$LUA" ]; then 34 | sudo apt-get install -qq -y liblua5.1-dev 35 | fi 36 | - sudo apt-get install -qq -y luarocks cppcheck valgrind 37 | - sudo apt-get install -qq -y cpanminus libipc-run3-perl > build.log 2>&1 || (cat build.log && exit 1) 38 | - sudo cpanm --notest Test::Base Test::LongString > build.log 2>&1 || (cat build.log && exit 1) 39 | 40 | script: 41 | #- cppcheck --force --error-exitcode=1 --enable=warning . > build.log 2>&1 || (cat build.log && exit 1) 42 | - cd lua-cjson/ && make && sudo make install && cd .. 43 | - make test valtest 44 | 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version=0.09 2 | 3 | name=lua-redis-parser 4 | dist=$(name)-$(version) 5 | 6 | LUA_VERSION = 5.1 7 | 8 | # See http://lua-users.org/wiki/BuildingModules for platform specific 9 | # details. 10 | 11 | ## Linux/BSD 12 | PREFIX ?= /usr/local 13 | LDFLAGS += -shared 14 | 15 | ## OSX (Macports) 16 | #PREFIX ?= /opt/local 17 | #LDFLAGS += -bundle -undefined dynamic_lookup 18 | 19 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 20 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 21 | 22 | #CFLAGS ?= -g -Wall -pedantic -fno-inline 23 | CFLAGS ?= -g -O -Wall 24 | override CFLAGS += -fpic -I$(LUA_INCLUDE_DIR) 25 | 26 | INSTALL ?= install 27 | 28 | .PHONY: all clean dist test t 29 | 30 | #CC = gcc 31 | RM = rm -f 32 | 33 | all: parser.so 34 | 35 | redis-parser.o: ddebug.h 36 | 37 | .c.o: 38 | $(CC) -c $(CFLAGS) $(CPPFLAGS) $(BUILD_CFLAGS) -o $@ $< 39 | 40 | parser.so: redis-parser.o 41 | $(CC) $(LDFLAGS) -o $@ $^ 42 | 43 | install: 44 | $(INSTALL) -d $(DESTDIR)$(LUA_LIB_DIR)/redis 45 | $(INSTALL) parser.so $(DESTDIR)$(LUA_LIB_DIR)/redis 46 | 47 | clean: 48 | $(RM) *.so *.o redis/*.so 49 | 50 | test: all 51 | $(INSTALL) -d redis 52 | $(INSTALL) parser.so redis/ 53 | prove -I. -r t 54 | 55 | valtest: parser.so 56 | $(INSTALL) -d redis 57 | $(INSTALL) parser.so redis/ 58 | TEST_LUA_USE_VALGRIND=1 prove -I. -r t 59 | 60 | t: parser.so 61 | $(INSTALL) -d redis 62 | $(INSTALL) parser.so redis/ 63 | TEST_LUA_USE_VALGRIND=1 prove -I. t/sanity.t 64 | 65 | dist: 66 | git archive --prefix="$(dist)/" master | \ 67 | gzip -9 > "$(dist).tar.gz" 68 | git archive --prefix="$(dist)/" \ 69 | -o "$(dist).zip" master 70 | 71 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | NAME 2 | ==== 3 | 4 | lua-redis-parser - Redis reply parser and request constructor library for Lua 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Version](#version) 10 | * [Description](#description) 11 | * [Functions](#functions) 12 | * [parse_reply](#parse_reply) 13 | * [parse_replies](#parse_replies) 14 | * [typename](#typename) 15 | * [build_query](#build_query) 16 | * [Constants](#constants) 17 | * [BAD_REPLY](#bad_reply) 18 | * [INTEGER_REPLY](#integer_reply) 19 | * [ERROR_REPLY](#error_reply) 20 | * [STATUS_REPLY](#status_reply) 21 | * [BULK_REPLY](#bulk_reply) 22 | * [MULTI_BULK_REPLY](#multi_bulk_reply) 23 | * [null](#null) 24 | * [Background](#background) 25 | * [Report Bugs](#report-bugs) 26 | * [Source Repository](#source-repository) 27 | * [Installation](#installation) 28 | * [Build requirements](#build-requirements) 29 | * [Linux/BSD/Solaris](#linuxbsdsolaris) 30 | * [Mac OS X](#mac-os-x) 31 | * [Author](#author) 32 | * [Copyright & License](#copyright--license) 33 | * [SEE ALSO](#see-also) 34 | 35 | Version 36 | ======= 37 | 38 | This document describes lua-redis-parser [v0.13](https://github.com/agentzh/lua-redis-parser/tags) released on 8 April 2017. 39 | 40 | Description 41 | =========== 42 | 43 | This lua library implements a thin and fast redis raw response parser 44 | that constructs corresponding lua data strucutres, as well as a 45 | function that constructs redis raw requests. 46 | 47 | To maximize speed, this module is implemented in pure C. 48 | 49 | This library is usually used by Lua code running atop [lua-nginx-module](http://github.com/chaoslawful/lua-nginx-module) to access 50 | redis backends though [redis2-nginx-module](http://github.com/agentzh/redis2-nginx-module). 51 | 52 | Functions 53 | ========= 54 | 55 | The `parser` variable used below is referring to the variable holding the return value of `require "redis.parser"`. In other words, we assume you have written the following code first: 56 | 57 | ```lua 58 | 59 | local parser = require "redis.parser" 60 | ``` 61 | 62 | [Back to TOC](#table-of-contents) 63 | 64 | parse_reply 65 | ----------- 66 | **syntax:** *res, typ = parser.parse_reply(raw_reply)* 67 | 68 | Parses the single (or the first) raw redis reply from the `raw_reply` string and returns the Lua data structure `res`, as well as the reply type `typ`. 69 | 70 | Here is an example: 71 | 72 | ```lua 73 | 74 | local parser = require 'redis.parser' 75 | 76 | -- assuming the reply variable holds the (single) redis response 77 | -- to be parsed: 78 | local res, typ = parser.parse_reply(reply) 79 | 80 | if typ == parser.BAD_REPLY then 81 | -- res is the textual error message from the parser 82 | elseif typ == parser.INTEGER_REPLY then 83 | -- res is an integer, like 3, returned from the redis server 84 | elseif typ == parser.ERROR_REPLY then 85 | -- res is the error message from the redis2 server 86 | elseif typ == parser.STATUS_REPLY then 87 | -- res is the textual message from the redis server 88 | elseif typ == parser.BULK_REPLY then 89 | --- res is a string for the bulk data 90 | elseif typ == parser.MULTI_BULK_REPLY then 91 | -- res is a lua (array) table that holds the individual bulks 92 | end 93 | ``` 94 | 95 | [Back to TOC](#table-of-contents) 96 | 97 | parse_replies 98 | ------------- 99 | **syntax:** *results = parser.parse_replies(raw_replies)* 100 | 101 | Similar to the [parse_reply](#parse_reply) method, but parse multiple pipelined redis replies in the `raw_replies` string argument. Returns a table of all the parsed results where each result is an array-like table consists of two elements, `res` and `typ`, which have exactly the same meaning as the return values of the [parse_reply](#parse_reply) function. 102 | 103 | For instance, 104 | 105 | ```lua 106 | 107 | local parser = require "redis.parser" 108 | 109 | -- assuming the replies variable holds n redis responses 110 | -- to be parsed: 111 | local results = parser.parse_replies(replies, n) 112 | for i, result in ipairs(results) do 113 | local res = result[1] 114 | local typ = result[2] 115 | 116 | -- res and typ have exactly the same meaning as in 117 | -- the parse_reply method documented above 118 | end 119 | ``` 120 | 121 | [Back to TOC](#table-of-contents) 122 | 123 | typename 124 | -------- 125 | **syntax:** *str = parser.typename(typ)* 126 | 127 | Returns the textual representation of the reply type values returned by the [parse_reply](#parse_reply) and [parse_replies](#parse_replies) functions. Here's the correspondence: 128 | 129 | ```lua 130 | 131 | parser.typename(parser.BAD_REPLY) == "bad reply" 132 | parser.typename(parser.INTEGER_REPLY) == "integer reply" 133 | parser.typename(parser.ERROR_REPLY) == "error reply" 134 | parser.typename(parser.STATUS_REPLY) == "status reply" 135 | parser.typename(parser.BULK_REPLY) == "bulk reply" 136 | parser.typename(parser.MULTI_BULK_REPLY) == "multi-bulk reply" 137 | ``` 138 | 139 | [Back to TOC](#table-of-contents) 140 | 141 | build_query 142 | ----------- 143 | **syntax:** *raw_request = parser.build_query(args)* 144 | 145 | Constructs a raw Redis request from simple Lua values. It simply accepts a Lua array-like table, a list of parameters including 146 | the command name. 147 | 148 | Please check out the complete list of redis 2.0 commands, 149 | 150 | 151 | 152 | The first command in that list, "APPEND key value", for example, we can just use 153 | 154 | ```lua 155 | 156 | local parser = require "redis.parser" 157 | 158 | local req = parser.build_query({'APPEND', 'some-key', 'some-value'}) 159 | ``` 160 | 161 | to construct a binary request in the return value. Because the Redis command is case insensitive, I usually just use 'append', the lower case form, as the first element of that list, as in 162 | 163 | ```lua 164 | 165 | local parser = require "redis.parser" 166 | 167 | local req = parser.build_query({'set', 'foo', 'some value'}) 168 | -- req is the raw TCP request ready to send to the remote redis server 169 | -- over the TCP connection 170 | ``` 171 | 172 | Null values should be specified by `parser.null` rather than Lua's `nil` value. 173 | 174 | Boolean values will be converted to `1` or `0`, for `true` and `false`, respectively. 175 | 176 | [Back to TOC](#table-of-contents) 177 | 178 | Constants 179 | ========= 180 | 181 | [Back to TOC](#table-of-contents) 182 | 183 | BAD_REPLY 184 | --------- 185 | **syntax:** *typ = parser.BAD_REPLY* 186 | 187 | [Back to TOC](#table-of-contents) 188 | 189 | INTEGER_REPLY 190 | ------------- 191 | **syntax:** *typ = parser.INTEGER_REPLY* 192 | 193 | [Back to TOC](#table-of-contents) 194 | 195 | ERROR_REPLY 196 | ----------- 197 | **syntax:** *typ = parser.ERROR_REPLY* 198 | 199 | [Back to TOC](#table-of-contents) 200 | 201 | STATUS_REPLY 202 | ------------ 203 | **syntax:** *typ = parser.STATUS_REPLY* 204 | 205 | [Back to TOC](#table-of-contents) 206 | 207 | BULK_REPLY 208 | ---------- 209 | **syntax:** *typ = parser.BULK_REPLY* 210 | 211 | [Back to TOC](#table-of-contents) 212 | 213 | MULTI_BULK_REPLY 214 | ---------------- 215 | **syntax:** *typ = parser.MULTI_BULK_REPLY* 216 | 217 | [Back to TOC](#table-of-contents) 218 | 219 | null 220 | ---- 221 | **syntax:** *val = parser.null* 222 | 223 | [Back to TOC](#table-of-contents) 224 | 225 | Background 226 | ========== 227 | 228 | This module is originally written for [lua-nginx-module](http://github.com/chaoslawful/lua-nginx-module) and [redis2-nginx-module](http://github.com/agentzh/redis2-nginx-module): 229 | 230 | [Back to TOC](#table-of-contents) 231 | 232 | Report Bugs 233 | =========== 234 | 235 | Although a lot of effort has been put into testing and code tuning, there must be some serious bugs lurking somewhere in this module. So whenever you are bitten by any quirks, please don't hesitate to 236 | 237 | 1. create a ticket on the [issue tracking interface](http://github.com/agentzh/lua-redis-parser/issues) provided by GitHub, 238 | 1. or send a bug report or even patches to `agentzh@gmail.com`. 239 | 240 | [Back to TOC](#table-of-contents) 241 | 242 | Source Repository 243 | ================= 244 | 245 | Available on GitHub at [agentzh/lua-redis-parser](http://github.com/agentzh/lua-redis-parser). 246 | 247 | [Back to TOC](#table-of-contents) 248 | 249 | Installation 250 | ============ 251 | 252 | [Back to TOC](#table-of-contents) 253 | 254 | Build requirements 255 | ------------------ 256 | 257 | * Lua (http://www.lua.org/) 258 | * or LuaJIT (http://www.luajit.org/) 259 | * Latest source tarball of this library downloaded from 260 | 261 | Gnu make and gcc is required to build this module. 262 | 263 | [Back to TOC](#table-of-contents) 264 | 265 | Linux/BSD/Solaris 266 | ----------------- 267 | 268 | ```bash 269 | 270 | gmake CC=gcc 271 | gmake install CC=gcc 272 | ``` 273 | 274 | [Back to TOC](#table-of-contents) 275 | 276 | Mac OS X 277 | -------- 278 | 279 | ```bash 280 | 281 | make LDFLAGS='-bundle -undefined dynamic_lookup' CC=gcc 282 | make install 283 | ``` 284 | 285 | If your Lua or LuaJIT is not installed into the system, specify its include directory like this: 286 | 287 | ```bash 288 | 289 | make LUA_INCLUDE_DIR=/opt/luajit/include/luajit-2.0 290 | ``` 291 | 292 | You can specify a custom path for the installation target: 293 | 294 | ```bash 295 | 296 | make install LUA_LIB_DIR=/opt/lualib 297 | ``` 298 | 299 | The `DESTDIR` variable is also supported, to ease RPM packaging. 300 | 301 | This library is included and enabled by default in the [OpenResty bundle](http://openresty.org). 302 | 303 | [Back to TOC](#table-of-contents) 304 | 305 | Author 306 | ====== 307 | 308 | * Yichun "agentzh" Zhang (章亦春) 309 | 310 | [Back to TOC](#table-of-contents) 311 | 312 | Copyright & License 313 | =================== 314 | 315 | This module is licenced under the BSD license. 316 | 317 | Copyright (C) 2009-2017, by Yichun "agentzh" Zhang (章亦春) , OpenResty Inc. 318 | 319 | All rights reserved. 320 | 321 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 322 | 323 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 324 | 325 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 326 | 327 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 328 | 329 | [Back to TOC](#table-of-contents) 330 | 331 | SEE ALSO 332 | ======== 333 | * Use case: [Dynamic Routing Based On Redis](http://openresty.org/#DynamicRoutingBasedOnRedis) 334 | * [lua-nginx-module](http://github.com/chaoslawful/lua-nginx-module) 335 | * [redis2-nginx-module](http://github.com/agentzh/redis2-nginx-module) 336 | * [Redis official site](http://redis.io/) 337 | 338 | -------------------------------------------------------------------------------- /ddebug.h: -------------------------------------------------------------------------------- 1 | #if defined(DDEBUG) && (DDEBUG) 2 | 3 | # include 4 | 5 | # define dd(...) \ 6 | fprintf(stderr, __VA_ARGS__); \ 7 | fprintf(stderr, "\n") 8 | 9 | #else 10 | 11 | # define dd(fmt, ...) 12 | 13 | #endif 14 | 15 | -------------------------------------------------------------------------------- /redis-parser.c: -------------------------------------------------------------------------------- 1 | #ifndef DDEBUG 2 | #define DDEBUG 0 3 | #endif 4 | #include "ddebug.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | #define LUA_REDIS_PARSER_VERSION "0.13" 14 | 15 | 16 | enum { 17 | BAD_REPLY = 0, 18 | STATUS_REPLY = 1, 19 | ERROR_REPLY = 2, 20 | INTEGER_REPLY = 3, 21 | BULK_REPLY = 4, 22 | MULTI_BULK_REPLY = 5 23 | }; 24 | 25 | 26 | enum { 27 | PARSE_OK = 0, 28 | PARSE_ERROR = 1 29 | }; 30 | 31 | 32 | static const char *redis_typenames[] = { 33 | "bad reply", 34 | "status reply", 35 | "error reply", 36 | "integer reply", 37 | "bulk reply", 38 | "multi-bulk reply" 39 | }; 40 | 41 | 42 | #define UINT64_LEN (sizeof("18446744073709551615") - 1) 43 | 44 | static void *redis_null = NULL; 45 | 46 | static int parse_reply_helper(lua_State *L, char **src, size_t len); 47 | static int redis_parse_reply(lua_State *L); 48 | static int redis_parse_replies(lua_State *L); 49 | static int redis_build_query(lua_State *L); 50 | static const char * parse_single_line_reply(const char *src, const char *last, 51 | size_t *dst_len); 52 | static const char * parse_bulk_reply(const char *src, const char *last, 53 | size_t *dst_len); 54 | static int parse_multi_bulk_reply(lua_State *L, char **src, 55 | const char *last); 56 | static size_t get_num_size(size_t i); 57 | static char *sprintf_num(char *dst, int64_t ui64); 58 | static int redis_typename(lua_State *L); 59 | 60 | 61 | #define redis_nelems(arr) \ 62 | (sizeof(arr) / sizeof(arr[0])) 63 | 64 | 65 | static const struct luaL_Reg redis_parser[] = { 66 | {"parse_reply", redis_parse_reply}, 67 | {"parse_replies", redis_parse_replies}, 68 | {"build_query", redis_build_query}, 69 | {"typename", redis_typename}, 70 | {NULL, NULL} 71 | }; 72 | 73 | 74 | int 75 | luaopen_redis_parser(lua_State *L) 76 | { 77 | luaL_register(L, "redis.parser", redis_parser); 78 | 79 | lua_pushliteral(L, LUA_REDIS_PARSER_VERSION); 80 | lua_setfield(L, -2, "_VERSION"); 81 | 82 | lua_pushlightuserdata(L, redis_null); 83 | lua_setfield(L, -2, "null"); 84 | 85 | lua_pushnumber(L, BAD_REPLY); 86 | lua_setfield(L, -2, "BAD_REPLY"); 87 | 88 | lua_pushnumber(L, STATUS_REPLY); 89 | lua_setfield(L, -2, "STATUS_REPLY"); 90 | 91 | lua_pushnumber(L, ERROR_REPLY); 92 | lua_setfield(L, -2, "ERROR_REPLY"); 93 | 94 | lua_pushnumber(L, INTEGER_REPLY); 95 | lua_setfield(L, -2, "INTEGER_REPLY"); 96 | 97 | lua_pushnumber(L, BULK_REPLY); 98 | lua_setfield(L, -2, "BULK_REPLY"); 99 | 100 | lua_pushnumber(L, MULTI_BULK_REPLY); 101 | lua_setfield(L, -2, "MULTI_BULK_REPLY"); 102 | 103 | return 1; 104 | } 105 | 106 | 107 | static int 108 | redis_parse_reply(lua_State *L) 109 | { 110 | char *p; 111 | size_t len; 112 | 113 | if (lua_gettop(L) != 1) { 114 | return luaL_error(L, "expected one argument but got %d", 115 | lua_gettop(L)); 116 | } 117 | 118 | p = (char *) luaL_checklstring(L, 1, &len); 119 | 120 | return parse_reply_helper(L, &p, len); 121 | } 122 | 123 | 124 | static int 125 | redis_parse_replies(lua_State *L) 126 | { 127 | char *p; 128 | char *q; 129 | int i, n, nret; 130 | size_t len; 131 | 132 | if (lua_gettop(L) != 2) { 133 | return luaL_error(L, "expected two arguments but got %d", 134 | lua_gettop(L)); 135 | } 136 | 137 | p = (char *) luaL_checklstring(L, 1, &len); 138 | 139 | n = luaL_checknumber(L, 2); 140 | 141 | dd("n = %d", (int) n); 142 | 143 | lua_pop(L, 1); 144 | 145 | lua_createtable(L, n, 0); /* table */ 146 | 147 | for (i = 1; i <= n; i++) { 148 | dd("parsing reply %d", i); 149 | 150 | lua_createtable(L, n, 2); /* table table */ 151 | q = p; 152 | nret = parse_reply_helper(L, &p, len); /* table table res typ */ 153 | if (nret != 2) { 154 | return luaL_error(L, "internal error: redis_parse_reply " 155 | "returns %d", nret); 156 | } 157 | 158 | dd("p = %p, q = %p, len = %d", p, q, (int) len); 159 | 160 | len -= p - q; 161 | 162 | dd("len is now %d", (int) len); 163 | 164 | lua_rawseti(L, -3, 2); /* table table res */ 165 | lua_rawseti(L, -2, 1); /* table table */ 166 | lua_rawseti(L, -2, i); /* table */ 167 | } 168 | 169 | return 1; 170 | } 171 | 172 | 173 | static int 174 | parse_reply_helper(lua_State *L, char **src, size_t len) 175 | { 176 | char *p; 177 | const char *last; 178 | const char *dst; 179 | size_t dst_len; 180 | lua_Number num; 181 | int rc; 182 | 183 | p = *src; 184 | 185 | if (len == 0) { 186 | lua_pushliteral(L, "empty reply"); 187 | lua_pushnumber(L, BAD_REPLY); 188 | return 2; 189 | } 190 | 191 | last = p + len; 192 | 193 | switch (*p) { 194 | case '+': 195 | p++; 196 | dst = parse_single_line_reply(p, last, &dst_len); 197 | 198 | if (dst_len == -2) { 199 | lua_pushliteral(L, "bad status reply"); 200 | lua_pushnumber(L, BAD_REPLY); 201 | return 2; 202 | } 203 | 204 | *src += dst_len + 1 + sizeof("\r\n") - 1; 205 | 206 | lua_pushlstring(L, dst, dst_len); 207 | lua_pushnumber(L, STATUS_REPLY); 208 | break; 209 | 210 | case '-': 211 | p++; 212 | dst = parse_single_line_reply(p, last, &dst_len); 213 | 214 | if (dst_len == -2) { 215 | lua_pushliteral(L, "bad error reply"); 216 | lua_pushnumber(L, BAD_REPLY); 217 | return 2; 218 | } 219 | 220 | *src += dst_len + 1 + sizeof("\r\n") - 1; 221 | 222 | lua_pushlstring(L, dst, dst_len); 223 | lua_pushnumber(L, ERROR_REPLY); 224 | break; 225 | 226 | case ':': 227 | p++; 228 | dst = parse_single_line_reply(p, last, &dst_len); 229 | 230 | if (dst_len == -2) { 231 | lua_pushliteral(L, "bad integer reply"); 232 | lua_pushnumber(L, BAD_REPLY); 233 | return 2; 234 | } 235 | 236 | *src += dst_len + 1 + sizeof("\r\n") - 1; 237 | 238 | lua_pushlstring(L, dst, dst_len); 239 | num = lua_tonumber(L, -1); 240 | lua_pop(L, 1); 241 | 242 | lua_pushnumber(L, num); 243 | lua_pushnumber(L, INTEGER_REPLY); 244 | break; 245 | 246 | case '$': 247 | p++; 248 | dst = parse_bulk_reply(p, last, &dst_len); 249 | 250 | if (dst_len == -2) { 251 | lua_pushliteral(L, "bad bulk reply"); 252 | lua_pushnumber(L, BAD_REPLY); 253 | return 2; 254 | } 255 | 256 | if (dst_len == -1) { 257 | *src = (char *) dst + sizeof("\r\n") - 1; 258 | 259 | /* lua_pushlightuserdata(L, redis_null); */ 260 | lua_pushnil(L); 261 | lua_pushnumber(L, BULK_REPLY); 262 | return 2; 263 | } 264 | 265 | *src = (char *) dst + dst_len + sizeof("\r\n") - 1; 266 | 267 | lua_pushlstring(L, dst, dst_len); 268 | lua_pushnumber(L, BULK_REPLY); 269 | break; 270 | 271 | case '*': 272 | p++; 273 | rc = parse_multi_bulk_reply(L, &p, last); 274 | 275 | if (rc != PARSE_OK) { 276 | lua_pushliteral(L, "bad multi bulk reply"); 277 | lua_pushnumber(L, BAD_REPLY); 278 | return 2; 279 | } 280 | 281 | /* rc == PARSE_OK */ 282 | 283 | *src = (char *) p; 284 | 285 | lua_pushnumber(L, MULTI_BULK_REPLY); 286 | break; 287 | 288 | default: 289 | lua_pushliteral(L, "invalid reply"); 290 | lua_pushnumber(L, BAD_REPLY); 291 | break; 292 | } 293 | 294 | return 2; 295 | } 296 | 297 | 298 | static const char * 299 | parse_single_line_reply(const char *src, const char *last, size_t *dst_len) 300 | { 301 | const char *p = src; 302 | int seen_cr = 0; 303 | 304 | while (p != last) { 305 | 306 | if (*p == '\r') { 307 | seen_cr = 1; 308 | 309 | } else if (seen_cr) { 310 | if (*p == '\n') { 311 | *dst_len = p - src - 1; 312 | return src; 313 | } 314 | 315 | seen_cr = 0; 316 | } 317 | 318 | p++; 319 | } 320 | 321 | /* CRLF not found at all */ 322 | *dst_len = -2; 323 | return NULL; 324 | } 325 | 326 | 327 | #define CHECK_EOF if (p >= last) goto invalid; 328 | 329 | 330 | static const char * 331 | parse_bulk_reply(const char *src, const char *last, size_t *dst_len) 332 | { 333 | const char *p = src; 334 | ssize_t size = 0; 335 | const char *dst; 336 | 337 | CHECK_EOF 338 | 339 | /* read the bulk size */ 340 | 341 | if (*p == '-') { 342 | p++; 343 | CHECK_EOF 344 | 345 | while (*p != '\r') { 346 | if (*p < '0' || *p > '9') { 347 | goto invalid; 348 | } 349 | 350 | p++; 351 | CHECK_EOF 352 | } 353 | 354 | /* *p == '\r' */ 355 | 356 | if (last - p < size + sizeof("\r\n") - 1) { 357 | goto invalid; 358 | } 359 | 360 | p++; 361 | 362 | if (*p++ != '\n') { 363 | goto invalid; 364 | } 365 | 366 | *dst_len = -1; 367 | return p - (sizeof("\r\n") - 1); 368 | } 369 | 370 | while (*p != '\r') { 371 | if (*p < '0' || *p > '9') { 372 | goto invalid; 373 | } 374 | 375 | size *= 10; 376 | size += *p - '0'; 377 | 378 | p++; 379 | CHECK_EOF 380 | } 381 | 382 | /* *p == '\r' */ 383 | 384 | p++; 385 | CHECK_EOF 386 | 387 | if (*p++ != '\n') { 388 | goto invalid; 389 | } 390 | 391 | /* read the bulk data */ 392 | 393 | if (last - p < size + sizeof("\r\n") - 1) { 394 | goto invalid; 395 | } 396 | 397 | dst = p; 398 | 399 | p += size; 400 | 401 | if (*p++ != '\r') { 402 | goto invalid; 403 | } 404 | 405 | if (*p++ != '\n') { 406 | goto invalid; 407 | } 408 | 409 | *dst_len = size; 410 | return dst; 411 | 412 | invalid: 413 | *dst_len = -2; 414 | return NULL; 415 | } 416 | 417 | 418 | static int 419 | parse_multi_bulk_reply(lua_State *L, char **src, const char *last) 420 | { 421 | const char *p = *src; 422 | int count = 0; 423 | int i; 424 | size_t dst_len; 425 | const char *dst; 426 | 427 | dd("enter multi bulk parser"); 428 | 429 | CHECK_EOF 430 | 431 | while (*p != '\r') { 432 | if (*p == '-') { 433 | 434 | p++; 435 | CHECK_EOF 436 | 437 | if (*p < '0' || *p > '9') { 438 | goto invalid; 439 | } 440 | 441 | while (*p != '\r') { 442 | p++; 443 | CHECK_EOF 444 | } 445 | 446 | count = -1; 447 | break; 448 | } 449 | 450 | if (*p < '0' || *p > '9') { 451 | dd("expecting digit, but found %c", *p); 452 | goto invalid; 453 | } 454 | 455 | count *= 10; 456 | count += *p - '0'; 457 | 458 | p++; 459 | CHECK_EOF 460 | } 461 | 462 | dd("count = %d", count); 463 | 464 | /* *p == '\r' */ 465 | 466 | p++; 467 | CHECK_EOF 468 | 469 | if (*p++ != '\n') { 470 | goto invalid; 471 | } 472 | 473 | dd("reading the individual bulks"); 474 | 475 | if (count == -1) { 476 | 477 | /* lua_pushlightuserdata(L, redis_null); */ 478 | lua_pushnil(L); 479 | 480 | *src = (char *) p; 481 | return PARSE_OK; 482 | } 483 | 484 | lua_createtable(L, count, 0); 485 | 486 | for (i = 1; i <= count; i++) { 487 | CHECK_EOF 488 | 489 | switch (*p) { 490 | case '+': 491 | case '-': 492 | case ':': 493 | p++; 494 | dst = parse_single_line_reply(p, last, &dst_len); 495 | break; 496 | 497 | case '$': 498 | p++; 499 | dst = parse_bulk_reply(p, last, &dst_len); 500 | break; 501 | 502 | default: 503 | goto invalid; 504 | } 505 | 506 | if (dst_len == -2) { 507 | dd("bulk %d reply parse fail for multi bulks", i); 508 | return PARSE_ERROR; 509 | } 510 | 511 | if (dst_len == -1) { 512 | lua_pushnil(L); 513 | p = dst + sizeof("\r\n") - 1; 514 | 515 | } else { 516 | lua_pushlstring(L, dst, dst_len); 517 | p = dst + dst_len + sizeof("\r\n") - 1; 518 | } 519 | 520 | lua_rawseti(L, -2, i); 521 | } 522 | 523 | *src = (char *) p; 524 | 525 | return PARSE_OK; 526 | 527 | invalid: 528 | return PARSE_ERROR; 529 | } 530 | 531 | 532 | static int 533 | redis_build_query(lua_State *L) 534 | { 535 | int i, n; 536 | size_t len, total; 537 | const char *p; 538 | char *last; 539 | char *buf; 540 | int flag; 541 | 542 | if (lua_gettop(L) != 1) { 543 | return luaL_error(L, "expected one argument but got %d", 544 | lua_gettop(L)); 545 | } 546 | 547 | luaL_checktype(L, 1, LUA_TTABLE); 548 | 549 | n = lua_objlen(L, 1); 550 | 551 | if (n == 0) { 552 | return luaL_error(L, "empty input param table"); 553 | } 554 | 555 | total = sizeof("*") - 1 556 | + get_num_size(n) 557 | + sizeof("\r\n") - 1 558 | ; 559 | 560 | for (i = 1; i <= n; i++) { 561 | lua_rawgeti(L, 1, i); 562 | 563 | dd("param type: %d (%d)", lua_type(L, -1), LUA_TUSERDATA); 564 | 565 | switch (lua_type(L, -1)) { 566 | case LUA_TSTRING: 567 | case LUA_TNUMBER: 568 | lua_tolstring(L, -1, &len); 569 | 570 | total += sizeof("$") - 1 571 | + get_num_size(len) 572 | + sizeof("\r\n") - 1 573 | + len 574 | + sizeof("\r\n") - 1 575 | ; 576 | 577 | break; 578 | 579 | case LUA_TBOOLEAN: 580 | total += sizeof("$1\r\n1\r\n") - 1; 581 | break; 582 | 583 | case LUA_TLIGHTUSERDATA: 584 | p = lua_touserdata(L, -1); 585 | dd("user data: %p", p); 586 | if (p == redis_null) { 587 | total += sizeof("$-1\r\n") - 1; 588 | break; 589 | } 590 | 591 | default: 592 | return luaL_error(L, "parameter %d is not a string, number, " 593 | "redis.parser.null, or boolean value", i); 594 | } 595 | 596 | lua_pop(L, 1); 597 | } 598 | 599 | buf = lua_newuserdata(L, total); /* lua_newuserdata never returns NULL */ 600 | 601 | last = buf; 602 | 603 | *last++ = '*'; 604 | last = sprintf_num(last, n); 605 | *last++ = '\r'; *last++ = '\n'; 606 | 607 | for (i = 1; i <= n; i++) { 608 | lua_rawgeti(L, 1, i); 609 | 610 | switch (lua_type(L, -1)) { 611 | case LUA_TSTRING: 612 | case LUA_TNUMBER: 613 | p = luaL_checklstring(L, -1, &len); 614 | 615 | *last++ = '$'; 616 | 617 | last = sprintf_num(last, len); 618 | 619 | *last++ = '\r'; *last++ = '\n'; 620 | 621 | memcpy(last, p, len); 622 | last += len; 623 | 624 | *last++ = '\r'; *last++ = '\n'; 625 | 626 | break; 627 | 628 | case LUA_TBOOLEAN: 629 | memcpy(last, "$1\r\n", sizeof("$1\r\n") - 1); 630 | last += sizeof("$1\r\n") - 1; 631 | 632 | flag = lua_toboolean(L, -1); 633 | *last++ = flag ? '1' : '0'; 634 | 635 | *last++ = '\r'; *last++ = '\n'; 636 | 637 | break; 638 | 639 | case LUA_TLIGHTUSERDATA: 640 | /* must be null */ 641 | memcpy(last, "$-1\r\n", sizeof("$-1\r\n") - 1); 642 | last += sizeof("$-1\r\n") - 1; 643 | break; 644 | 645 | default: 646 | /* cannot reach here */ 647 | break; 648 | } 649 | 650 | lua_pop(L, 1); 651 | } 652 | 653 | if (last - buf != (ssize_t) total) { 654 | return luaL_error(L, "buffer error"); 655 | } 656 | 657 | lua_pushlstring(L, buf, total); 658 | 659 | return 1; 660 | } 661 | 662 | 663 | static size_t 664 | get_num_size(size_t i) 665 | { 666 | size_t n = 0; 667 | 668 | do { 669 | i = i / 10; 670 | n++; 671 | } while (i > 0); 672 | 673 | return n; 674 | } 675 | 676 | 677 | static char * 678 | sprintf_num(char *dst, int64_t ui64) 679 | { 680 | char *p; 681 | char temp[UINT64_LEN + 1]; 682 | size_t len; 683 | 684 | p = temp + UINT64_LEN; 685 | 686 | do { 687 | *--p = (char) (ui64 % 10 + '0'); 688 | } while (ui64 /= 10); 689 | 690 | len = (temp + UINT64_LEN) - p; 691 | 692 | memcpy(dst, p, len); 693 | 694 | return dst + len; 695 | } 696 | 697 | 698 | static int 699 | redis_typename(lua_State *L) 700 | { 701 | int typ; 702 | 703 | if (lua_gettop(L) != 1) { 704 | return luaL_error(L, "expecting one argument but got %d", 705 | lua_gettop(L)); 706 | } 707 | 708 | typ = (int) luaL_checkinteger(L, 1); 709 | 710 | if (typ < 0 || typ >= redis_nelems(redis_typenames)) { 711 | lua_pushnil(L); 712 | return 1; 713 | } 714 | 715 | lua_pushstring(L, redis_typenames[typ]); 716 | return 1; 717 | } 718 | 719 | -------------------------------------------------------------------------------- /t/RedisParser.pm: -------------------------------------------------------------------------------- 1 | package t::RedisParser; 2 | 3 | use Test::Base -Base; 4 | use IPC::Run3; 5 | use Cwd; 6 | 7 | use Test::LongString; 8 | 9 | our @EXPORT = qw( run_tests ); 10 | 11 | $ENV{LUA_CPATH} = "?.so;" . ($ENV{LUA_CPATH} || "") . ';' . "/usr/local/openresty-debug/lualib/?.so;/usr/local/openresty/lualib/?.so;;"; 12 | #$ENV{LUA_PATH} = ($ENV{LUA_PATH} || "" ) . ';' . getcwd . "/runtime/?.lua" . ';;'; 13 | 14 | sub run_test ($) { 15 | my $block = shift; 16 | #print $json_xs->pretty->encode(\@new_rows); 17 | #my $res = #print $json_xs->pretty->encode($res); 18 | my $name = $block->name; 19 | 20 | my $lua = $block->lua or 21 | die "No --- lua specified for test $name\n"; 22 | 23 | open my $fh, ">test_case.lua"; 24 | print $fh $lua; 25 | close $fh; 26 | 27 | my ($res, $err); 28 | 29 | my @cmd; 30 | 31 | if ($ENV{TEST_LUA_USE_VALGRIND}) { 32 | @cmd = ('valgrind', '-q', '--leak-check=full', 'lua', 'test_case.lua'); 33 | } else { 34 | @cmd = ('lua', 'test_case.lua'); 35 | } 36 | 37 | run3 \@cmd, undef, \$res, \$err; 38 | 39 | #warn "res:$res\nerr:$err\n"; 40 | 41 | if (defined $block->err) { 42 | $err =~ /.*:.*:.*: (.*\s)?/; 43 | $err = $1; 44 | is $err, $block->err, "$name - err expected"; 45 | } elsif ($?) { 46 | die "Failed to execute --- lua for test $name: $err\n"; 47 | 48 | } else { 49 | #is $res, $block->out, "$name - output ok"; 50 | is_string $res, $block->out, "$name - output ok"; 51 | } 52 | 53 | unlink 'test_case.lua' or warn "could not delete \'test_case.lua\':$!"; 54 | } 55 | 56 | sub run_tests () { 57 | for my $block (blocks()) { 58 | run_test($block); 59 | } 60 | } 61 | 62 | 1; 63 | -------------------------------------------------------------------------------- /t/pipeline.t: -------------------------------------------------------------------------------- 1 | # vi:ft= 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use t::RedisParser; 7 | plan tests => 1 * blocks(); 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: single reply parsed by parse_replies 14 | --- lua 15 | parser = require("redis.parser") 16 | replies = '+OK\r\n' 17 | results = parser.parse_replies(replies, 1) 18 | print("res count == " .. #results) 19 | print("res[1] count == " .. #results[1]) 20 | local res = results[1] 21 | print("res[1][1] == " .. res[1]) 22 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.STATUS_REPLY) 23 | --- out 24 | res count == 1 25 | res[1] count == 2 26 | res[1][1] == OK 27 | res[1][2] == 1 == 1 28 | 29 | 30 | 31 | === TEST 2: single bad reply parsed by parse_replies 32 | --- lua 33 | parser = require("redis.parser") 34 | replies = '+OK' 35 | results = parser.parse_replies(replies, 1) 36 | print("res count == " .. #results) 37 | print("res[1] count == " .. #results[1]) 38 | local res = results[1] 39 | print("res[1][1] == " .. res[1]) 40 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.BAD_REPLY) 41 | --- out 42 | res count == 1 43 | res[1] count == 2 44 | res[1][1] == bad status reply 45 | res[1][2] == 0 == 0 46 | 47 | 48 | 49 | === TEST 3: multiple status replies 50 | --- lua 51 | parser = require("redis.parser") 52 | replies = '+OK\r\n+DONE\r\n' 53 | results = parser.parse_replies(replies, 2) 54 | print("res count == " .. #results) 55 | print("res[1] count == " .. #results[1]) 56 | print("res[2] count == " .. #results[2]) 57 | 58 | local res = results[1] 59 | 60 | print("res[1][1] == " .. res[1]) 61 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.STATUS_REPLY) 62 | 63 | res = results[2] 64 | 65 | print("res[2][1] == " .. res[1]) 66 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.STATUS_REPLY) 67 | 68 | --- out 69 | res count == 2 70 | res[1] count == 2 71 | res[2] count == 2 72 | res[1][1] == OK 73 | res[1][2] == 1 == 1 74 | res[2][1] == DONE 75 | res[2][2] == 1 == 1 76 | 77 | 78 | 79 | === TEST 4: multiple integer replies 80 | --- lua 81 | parser = require("redis.parser") 82 | replies = ':-32\r\n:532\r\n' 83 | results = parser.parse_replies(replies, 2) 84 | print("res count == " .. #results) 85 | print("res[1] count == " .. #results[1]) 86 | print("res[2] count == " .. #results[2]) 87 | 88 | local res = results[1] 89 | 90 | print("res[1][1] == " .. res[1]) 91 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.INTEGER_REPLY) 92 | 93 | res = results[2] 94 | 95 | print("res[2][1] == " .. res[1]) 96 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.INTEGER_REPLY) 97 | 98 | --- out 99 | res count == 2 100 | res[1] count == 2 101 | res[2] count == 2 102 | res[1][1] == -32 103 | res[1][2] == 3 == 3 104 | res[2][1] == 532 105 | res[2][2] == 3 == 3 106 | 107 | 108 | 109 | === TEST 5: multiple error replies 110 | --- lua 111 | parser = require("redis.parser") 112 | replies = '-ERROR\r\n-BAD\r\n' 113 | results = parser.parse_replies(replies, 2) 114 | print("res count == " .. #results) 115 | print("res[1] count == " .. #results[1]) 116 | print("res[2] count == " .. #results[2]) 117 | 118 | local res = results[1] 119 | 120 | print("res[1][1] == " .. res[1]) 121 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.ERROR_REPLY) 122 | 123 | res = results[2] 124 | 125 | print("res[2][1] == " .. res[1]) 126 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.ERROR_REPLY) 127 | 128 | --- out 129 | res count == 2 130 | res[1] count == 2 131 | res[2] count == 2 132 | res[1][1] == ERROR 133 | res[1][2] == 2 == 2 134 | res[2][1] == BAD 135 | res[2][2] == 2 == 2 136 | 137 | 138 | 139 | === TEST 6: multiple bad replies (invalid reply) 140 | --- lua 141 | parser = require("redis.parser") 142 | replies = '\r\n-BAD\r\n' 143 | results = parser.parse_replies(replies, 2) 144 | print("res count == " .. #results) 145 | print("res[1] count == " .. #results[1]) 146 | print("res[2] count == " .. #results[2]) 147 | 148 | local res = results[1] 149 | 150 | print("res[1][1] == " .. res[1]) 151 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.BAD_REPLY) 152 | 153 | res = results[2] 154 | 155 | print("res[2][1] == " .. res[1]) 156 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.BAD_REPLY) 157 | 158 | --- out 159 | res count == 2 160 | res[1] count == 2 161 | res[2] count == 2 162 | res[1][1] == invalid reply 163 | res[1][2] == 0 == 0 164 | res[2][1] == invalid reply 165 | res[2][2] == 0 == 0 166 | 167 | 168 | 169 | === TEST 7: multiple bad replies (empty reply) 170 | --- lua 171 | parser = require("redis.parser") 172 | replies = '' 173 | results = parser.parse_replies(replies, 2) 174 | print("res count == " .. #results) 175 | print("res[1] count == " .. #results[1]) 176 | print("res[2] count == " .. #results[2]) 177 | 178 | local res = results[1] 179 | 180 | print("res[1][1] == " .. res[1]) 181 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.BAD_REPLY) 182 | 183 | res = results[2] 184 | 185 | print("res[2][1] == " .. res[1]) 186 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.BAD_REPLY) 187 | 188 | --- out 189 | res count == 2 190 | res[1] count == 2 191 | res[2] count == 2 192 | res[1][1] == empty reply 193 | res[1][2] == 0 == 0 194 | res[2][1] == empty reply 195 | res[2][2] == 0 == 0 196 | 197 | 198 | 199 | === TEST 8: multiple bulk replies 200 | --- lua 201 | parser = require("redis.parser") 202 | replies = '$-1\r\n$5\r\nhello\r\n' 203 | results = parser.parse_replies(replies, 2) 204 | print("res count == " .. #results) 205 | print("res[1] count == " .. #results[1]) 206 | print("res[2] count == " .. #results[2]) 207 | 208 | local res = results[1] 209 | 210 | print("res[1][1] == " .. (res[1] or "nil")) 211 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.BULK_REPLY) 212 | 213 | res = results[2] 214 | 215 | print("res[2][1] == " .. res[1]) 216 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.BULK_REPLY) 217 | 218 | --- out 219 | res count == 2 220 | res[1] count == 2 221 | res[2] count == 2 222 | res[1][1] == nil 223 | res[1][2] == 4 == 4 224 | res[2][1] == hello 225 | res[2][2] == 4 == 4 226 | 227 | 228 | 229 | === TEST 9: multiple multi-bulk replies 230 | --- lua 231 | cjson = require('cjson') 232 | parser = require("redis.parser") 233 | replies = '*2\r\n$-1\r\n$5\r\nhello\r\n*1\r\n$1\r\na\r\n$2\r\nef\r\n' 234 | results = parser.parse_replies(replies, 2) 235 | print("res count == " .. #results) 236 | print("res[1] count == " .. #results[1]) 237 | print("res[2] count == " .. #results[2]) 238 | 239 | local res = results[1] 240 | 241 | print("res[1][1] == " .. cjson.encode(res[1])) 242 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.MULTI_BULK_REPLY) 243 | 244 | res = results[2] 245 | 246 | print("res[2][1] == " .. cjson.encode(res[1])) 247 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.MULTI_BULK_REPLY) 248 | 249 | --- out 250 | res count == 2 251 | res[1] count == 2 252 | res[2] count == 2 253 | res[1][1] == [null,"hello"] 254 | res[1][2] == 5 == 5 255 | res[2][1] == ["a"] 256 | res[2][2] == 5 == 5 257 | 258 | 259 | 260 | === TEST 10: nil multi bulk reply and status reply 261 | --- lua 262 | parser = require("redis.parser") 263 | replies = '*-1\r\n+DONE\r\n' 264 | results = parser.parse_replies(replies, 2) 265 | print("res count == " .. #results) 266 | print("res[1] count == " .. #results[1]) 267 | print("res[2] count == " .. #results[2]) 268 | 269 | local res = results[1] 270 | 271 | print("res[1][1] == " .. (res[1] or "nil")) 272 | print("res[1][2] == " .. res[2] .. ' == ' .. parser.MULTI_BULK_REPLY) 273 | 274 | res = results[2] 275 | 276 | print("res[2][1] == " .. res[1]) 277 | print("res[2][2] == " .. res[2] .. ' == ' .. parser.STATUS_REPLY) 278 | 279 | --- out 280 | res count == 2 281 | res[1] count == 2 282 | res[2] count == 2 283 | res[1][1] == nil 284 | res[1][2] == 5 == 5 285 | res[2][1] == DONE 286 | res[2][2] == 1 == 1 287 | 288 | -------------------------------------------------------------------------------- /t/sanity.t: -------------------------------------------------------------------------------- 1 | # vi:ft= 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use t::RedisParser; 7 | plan tests => 1 * blocks(); 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: no crlf in status reply 14 | --- lua 15 | parser = require("redis.parser") 16 | reply = '+OK' 17 | res, typ = parser.parse_reply(reply) 18 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.BAD_REPLY) 19 | print("res == " .. res) 20 | --- out 21 | typ == 0 == bad reply == 0 22 | res == bad status reply 23 | 24 | 25 | 26 | === TEST 2: good status reply 27 | --- lua 28 | parser = require("redis.parser") 29 | reply = '+OK\r\n' 30 | res, typ = parser.parse_reply(reply) 31 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.STATUS_REPLY) 32 | print("res == " .. res) 33 | --- out 34 | typ == 1 == status reply == 1 35 | res == OK 36 | 37 | 38 | 39 | === TEST 3: good error reply 40 | --- lua 41 | parser = require("redis.parser") 42 | reply = '-Bad argument\rHey\r\nblah blah blah\r\n' 43 | res, typ = parser.parse_reply(reply) 44 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.ERROR_REPLY) 45 | print("res == " .. res) 46 | --- out eval 47 | "typ == 2 == error reply == 2 48 | res == Bad argument\rHey\n" 49 | 50 | 51 | 52 | === TEST 4: good integer reply 53 | --- lua 54 | parser = require("redis.parser") 55 | reply = ':-32\r\n' 56 | res, typ = parser.parse_reply(reply) 57 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.INTEGER_REPLY) 58 | print("res == " .. res) 59 | print("res type == " .. type(res)) 60 | --- out 61 | typ == 3 == integer reply == 3 62 | res == -32 63 | res type == number 64 | 65 | 66 | 67 | === TEST 5: non-numeric integer reply 68 | --- lua 69 | parser = require("redis.parser") 70 | reply = ':abc\r\n' 71 | res, typ = parser.parse_reply(reply) 72 | print("typ == " .. typ .. ' == ' .. parser.INTEGER_REPLY) 73 | print("res == " .. res) 74 | print("res type == " .. type(res)) 75 | --- out 76 | typ == 3 == 3 77 | res == 0 78 | res type == number 79 | 80 | 81 | 82 | === TEST 6: bad integer reply 83 | --- lua 84 | parser = require("redis.parser") 85 | reply = ':12\r' 86 | res, typ = parser.parse_reply(reply) 87 | print("typ == " .. typ .. ' == ' .. parser.BAD_REPLY) 88 | print("res == " .. res) 89 | print("res type == " .. type(res)) 90 | --- out 91 | typ == 0 == 0 92 | res == bad integer reply 93 | res type == string 94 | 95 | 96 | 97 | === TEST 7: good bulk reply 98 | --- lua 99 | parser = require("redis.parser") 100 | reply = '$5\r\nhello\r\n' 101 | res, typ = parser.parse_reply(reply) 102 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.BULK_REPLY) 103 | print("res == " .. res) 104 | --- out 105 | typ == 4 == bulk reply == 4 106 | res == hello 107 | 108 | 109 | 110 | === TEST 8: good bulk reply (ignoring trailing stuffs) 111 | --- lua 112 | parser = require("redis.parser") 113 | reply = '$5\r\nhello\r\nblah' 114 | res, typ = parser.parse_reply(reply) 115 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 116 | print("res == " .. res) 117 | --- out 118 | typ == 4 == 4 119 | res == hello 120 | 121 | 122 | 123 | === TEST 9: bad bulk reply (bad bulk size) 124 | --- lua 125 | parser = require("redis.parser") 126 | reply = '$3b\r\nhello\r\nblah' 127 | res, typ = parser.parse_reply(reply) 128 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 129 | print("res == " .. res) 130 | --- out 131 | typ == 0 == 4 132 | res == bad bulk reply 133 | 134 | 135 | 136 | === TEST 10: bad bulk reply (bulk size too small) 137 | --- lua 138 | parser = require("redis.parser") 139 | reply = '$3\r\nhello\r\nblah' 140 | res, typ = parser.parse_reply(reply) 141 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 142 | print("res == " .. res) 143 | --- out 144 | typ == 0 == 4 145 | res == bad bulk reply 146 | 147 | 148 | 149 | === TEST 11: bad bulk reply (bulk size too large) 150 | --- lua 151 | parser = require("redis.parser") 152 | reply = '$6\r\nhello\r\nblah' 153 | res, typ = parser.parse_reply(reply) 154 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 155 | print("res == " .. res) 156 | --- out 157 | typ == 0 == 4 158 | res == bad bulk reply 159 | 160 | 161 | 162 | === TEST 12: bad bulk reply (bulk size too large, 2) 163 | --- lua 164 | parser = require("redis.parser") 165 | reply = '$7\r\nhello\r\nblah' 166 | res, typ = parser.parse_reply(reply) 167 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 168 | print("res == " .. res) 169 | --- out 170 | typ == 0 == 4 171 | res == bad bulk reply 172 | 173 | 174 | 175 | === TEST 13: bad bulk reply (bulk size too large, 3) 176 | --- lua 177 | parser = require("redis.parser") 178 | reply = '$8\r\nhello\r\nblah' 179 | res, typ = parser.parse_reply(reply) 180 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 181 | print("res == " .. res) 182 | --- out 183 | typ == 0 == 4 184 | res == bad bulk reply 185 | 186 | 187 | 188 | === TEST 14: good bulk reply (nil value) 189 | --- lua 190 | parser = require("redis.parser") 191 | reply = '$-1\r\n' 192 | res, typ = parser.parse_reply(reply) 193 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 194 | print("res", res) 195 | --- out eval 196 | "typ == 4 == 4 197 | res\tnil\n" 198 | 199 | 200 | 201 | === TEST 15: good bulk reply (nil value, -25 size) 202 | --- lua 203 | parser = require("redis.parser") 204 | reply = '$-25\r\n' 205 | res, typ = parser.parse_reply(reply) 206 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 207 | print("res", res) 208 | --- out eval 209 | "typ == 4 == 4 210 | res\tnil\n" 211 | 212 | 213 | 214 | === TEST 16: bad bulk reply (nil value, -1 size) 215 | --- lua 216 | parser = require("redis.parser") 217 | reply = '$-1\r' 218 | res, typ = parser.parse_reply(reply) 219 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 220 | print("res", res) 221 | --- out eval 222 | "typ == 0 == 4 223 | res\tbad bulk reply\n" 224 | 225 | 226 | 227 | === TEST 17: bad bulk reply (nil value, -1 size) 228 | --- lua 229 | parser = require("redis.parser") 230 | reply = '$-1\ra' 231 | res, typ = parser.parse_reply(reply) 232 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 233 | print("res", res) 234 | --- out eval 235 | "typ == 0 == 4 236 | res\tbad bulk reply\n" 237 | 238 | 239 | 240 | === TEST 18: bad bulk reply (nil value, -1 size) 241 | --- lua 242 | parser = require("redis.parser") 243 | reply = '$-1ab' 244 | res, typ = parser.parse_reply(reply) 245 | print("typ == " .. typ .. ' == ' .. parser.BULK_REPLY) 246 | print("res", res) 247 | --- out eval 248 | "typ == 0 == 4 249 | res\tbad bulk reply\n" 250 | 251 | 252 | 253 | === TEST 19: good multi bulk reply (1 bulk) 254 | --- lua 255 | cjson = require('cjson') 256 | parser = require("redis.parser") 257 | reply = '*1\r\n$1\r\na\r\n' 258 | res, typ = parser.parse_reply(reply) 259 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.MULTI_BULK_REPLY) 260 | print("res == " .. cjson.encode(res)) 261 | --- out eval 262 | qq{typ == 5 == multi-bulk reply == 5 263 | res == ["a"]\n} 264 | 265 | 266 | 267 | === TEST 20: good multi bulk reply (4 bulks) 268 | --- lua 269 | cjson = require('cjson') 270 | parser = require("redis.parser") 271 | reply = '*4\r\n$1\r\na\r\n$-1\r\n$0\r\n\r\n$5\r\nhello\r\n' 272 | res, typ = parser.parse_reply(reply) 273 | print("typ == " .. typ .. ' == ' .. parser.MULTI_BULK_REPLY) 274 | print("res == " .. cjson.encode(res)) 275 | print("res[2]:", res[2]); 276 | --- out eval 277 | qq{typ == 5 == 5 278 | res == ["a",null,"","hello"] 279 | res[2]:\tnil\n} 280 | 281 | 282 | 283 | === TEST 21: bad multi bulk reply (4 bulks) 284 | --- lua 285 | cjson = require('cjson') 286 | parser = require("redis.parser") 287 | reply = '*4\r\n$1\r\na\r\n$-1\r\n$0\r\n\n$5\r\nhello\r\n' 288 | res, typ = parser.parse_reply(reply) 289 | print("typ == " .. typ .. ' == ' .. parser.MULTI_BULK_REPLY) 290 | print("res == " .. cjson.encode(res)) 291 | --- out eval 292 | qq{typ == 0 == 5 293 | res == "bad multi bulk reply"\n} 294 | 295 | 296 | 297 | === TEST 22: bad multi bulk reply (4 bulks) 298 | --- lua 299 | cjson = require('cjson') 300 | parser = require("redis.parser") 301 | reply = '*6\r\n$1\r\na\r\n$-1\r\n$0\r\n\n$5\r\nhello\r\n' 302 | res, typ = parser.parse_reply(reply) 303 | print("typ == " .. typ .. ' == ' .. parser.MULTI_BULK_REPLY) 304 | print("res == " .. cjson.encode(res)) 305 | --- out eval 306 | qq{typ == 0 == 5 307 | res == "bad multi bulk reply"\n} 308 | 309 | 310 | 311 | === TEST 23: bad multi bulk reply (4 bulks) 312 | --- lua 313 | cjson = require('cjson') 314 | parser = require("redis.parser") 315 | reply = '*6\n$1\r\na\r\n$-1\r\n$0\r\n\n$5\r\nhello\r\n' 316 | res, typ = parser.parse_reply(reply) 317 | print("typ == " .. typ .. ' == ' .. parser.MULTI_BULK_REPLY) 318 | print("res == " .. cjson.encode(res)) 319 | --- out eval 320 | qq{typ == 0 == 5 321 | res == "bad multi bulk reply"\n} 322 | 323 | 324 | 325 | === TEST 24: bad multi bulk reply (4 bulks) 326 | --- lua 327 | cjson = require('cjson') 328 | parser = require("redis.parser") 329 | reply = '*6$1\r\na\r\n$-1\r\n$0\r\n\n$5\r\nhello\r\n' 330 | res, typ = parser.parse_reply(reply) 331 | print("typ == " .. typ .. ' == ' .. parser.MULTI_BULK_REPLY) 332 | print("res == " .. cjson.encode(res)) 333 | --- out eval 334 | qq{typ == 0 == 5 335 | res == "bad multi bulk reply"\n} 336 | 337 | 338 | 339 | === TEST 25: build query (empty param table) 340 | --- lua 341 | cjson = require('cjson') 342 | parser = require("redis.parser") 343 | q = {} 344 | local query = parser.build_query(q) 345 | print("query == " .. cjson.encode(query)) 346 | --- err 347 | empty input param table 348 | 349 | 350 | 351 | === TEST 26: build query (single param) 352 | --- lua 353 | cjson = require('cjson') 354 | parser = require("redis.parser") 355 | q = {'ping'} 356 | local query = parser.build_query(q) 357 | print("query == " .. cjson.encode(query)) 358 | --- out 359 | query == "*1\r\n$4\r\nping\r\n" 360 | 361 | 362 | 363 | === TEST 27: build query (single param) 364 | --- lua 365 | cjson = require('cjson') 366 | parser = require("redis.parser") 367 | q = {'get', 'one', '\r\n'} 368 | local query = parser.build_query(q) 369 | print("query == " .. cjson.encode(query)) 370 | --- out 371 | query == "*3\r\n$3\r\nget\r\n$3\r\none\r\n$2\r\n\r\n\r\n" 372 | 373 | 374 | 375 | === TEST 28: build query (empty param "") 376 | --- lua 377 | cjson = require('cjson') 378 | parser = require("redis.parser") 379 | q = {''} 380 | local query = parser.build_query(q) 381 | print("query == " .. cjson.encode(query)) 382 | --- out 383 | query == "*1\r\n$0\r\n\r\n" 384 | 385 | 386 | 387 | === TEST 29: build query (empty param "") 388 | --- lua 389 | cjson = require('cjson') 390 | parser = require("redis.parser") 391 | q = {''} 392 | local query = parser.build_query(q) 393 | print("query == " .. cjson.encode(query)) 394 | --- out 395 | query == "*1\r\n$0\r\n\r\n" 396 | 397 | 398 | 399 | === TEST 30: build query (nil param) 400 | --- lua 401 | cjson = require('cjson') 402 | parser = require("redis.parser") 403 | q = {parser.null} 404 | local query = parser.build_query(q) 405 | print("query == " .. cjson.encode(query)) 406 | --- out 407 | query == "*1\r\n$-1\r\n" 408 | 409 | 410 | 411 | === TEST 31: build query (numeric param) 412 | --- lua 413 | cjson = require('cjson') 414 | parser = require("redis.parser") 415 | q = {'set', 'foo', 3.1415926} 416 | local query = parser.build_query(q) 417 | print("query == " .. cjson.encode(query)) 418 | --- out 419 | query == "*3\r\n$3\r\nset\r\n$3\r\nfoo\r\n$9\r\n3.1415926\r\n" 420 | 421 | 422 | 423 | === TEST 32: multi bulk reply contains single line reply 424 | --- lua 425 | cjson = require('cjson') 426 | parser = require("redis.parser") 427 | reply = '*5\r\n$1\r\na\r\n:1\r\n-Bad argument\r\n+32\r\n$3\r\nfoo\r\n' 428 | res, typ = parser.parse_reply(reply) 429 | print("typ == " .. typ .. ' == ' .. parser.MULTI_BULK_REPLY) 430 | print("res == " .. cjson.encode(res)) 431 | --- out eval 432 | qq{typ == 5 == 5 433 | res == ["a","1","Bad argument","32","foo"]\n} 434 | 435 | 436 | 437 | === TEST 33: we allow left-over bytes 438 | --- lua 439 | cjson = require('cjson') 440 | parser = require("redis.parser") 441 | reply = '*3\r\n$1\r\na\r\n:1\r\n-Bad argument\r\n+32\r\n$3\r\nfoo\r\n' 442 | res, typ = parser.parse_reply(reply) 443 | print("typ == " .. typ .. ' == ' .. parser.MULTI_BULK_REPLY) 444 | print("res == " .. cjson.encode(res)) 445 | --- out eval 446 | qq{typ == 5 == 5 447 | res == ["a","1","Bad argument"]\n} 448 | 449 | 450 | 451 | === TEST 34: bug reported by James Hurst 452 | --- lua 453 | cjson = require('cjson') 454 | parser = require("redis.parser") 455 | reply = "*3\r\n$9\r\nsubscribe\r\n$38\r\nledge:d1d0ed5f3251473795548ab392181d06\r\n:1\r\n*3\r\n$7\r\nmessage\r\n$38\r\nledge:d1d0ed5f3251473795548ab392181d06\r\n$8\r\nfinished\r\n" 456 | resp = parser.parse_replies(reply, 2) 457 | print("res == " .. cjson.encode(resp)) 458 | --- out 459 | res == [[["subscribe","ledge:d1d0ed5f3251473795548ab392181d06","1"],5],[["message","ledge:d1d0ed5f3251473795548ab392181d06","finished"],5]] 460 | 461 | 462 | 463 | === TEST 35: bad typ 464 | --- lua 465 | local parser = require "redis.parser" 466 | print(parser.typename(-1)) 467 | print(parser.typename(6)) 468 | --- out 469 | nil 470 | nil 471 | 472 | 473 | 474 | === TEST 36: empty multi bulk reply (0 bulk) 475 | --- lua 476 | cjson = require('cjson') 477 | parser = require("redis.parser") 478 | reply = '*0\r\n' 479 | res, typ = parser.parse_reply(reply) 480 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.MULTI_BULK_REPLY) 481 | print("res == " .. cjson.encode(res)) 482 | --- out eval 483 | qq{typ == 5 == multi-bulk reply == 5 484 | res == \{\}\n} 485 | 486 | 487 | 488 | === TEST 37: nil multi bulk reply (-1 bulk) 489 | --- lua 490 | cjson = require('cjson') 491 | parser = require("redis.parser") 492 | reply = '*-1\r\n' 493 | res, typ = parser.parse_reply(reply) 494 | print("typ == " .. typ .. ' == ' .. parser.typename(typ) .. ' == ' .. parser.MULTI_BULK_REPLY) 495 | print("res == " .. cjson.encode(res)) 496 | --- out eval 497 | qq{typ == 5 == multi-bulk reply == 5 498 | res == null\n} 499 | 500 | 501 | 502 | === TEST 38: many query arguments 503 | --- lua 504 | cjson = require('cjson') 505 | parser = require("redis.parser") 506 | q = {} 507 | for i = 1,2048 do 508 | table.insert(q, "a") 509 | end 510 | local query = parser.build_query(q) 511 | print(string.len(query)) 512 | --- out 513 | 14343 514 | 515 | -------------------------------------------------------------------------------- /t/version.t: -------------------------------------------------------------------------------- 1 | # vi:ft= 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use t::RedisParser; 7 | plan tests => 1 * blocks(); 8 | 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: no crlf in status reply 14 | --- lua 15 | parser = require("redis.parser") 16 | print(parser._VERSION) 17 | --- out 18 | 0.13 19 | --------------------------------------------------------------------------------