iodine 0.3.6 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of iodine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/LIMITS.md +25 -0
- data/README.md +39 -80
- data/SPEC-Websocket-Draft.md +129 -4
- data/bin/echo +2 -2
- data/bin/http-hello +1 -0
- data/bin/updated api +113 -0
- data/bin/ws-echo +0 -1
- data/examples/broadcast.ru +56 -0
- data/examples/echo.ru +57 -0
- data/examples/hello.ru +30 -0
- data/examples/redis.ru +69 -0
- data/examples/shootout.ru +53 -0
- data/exe/iodine +2 -80
- data/ext/iodine/defer.c +11 -5
- data/ext/iodine/empty.h +26 -0
- data/ext/iodine/evio.h +1 -1
- data/ext/iodine/facil.c +103 -61
- data/ext/iodine/facil.h +20 -12
- data/ext/iodine/fio_dict.c +446 -0
- data/ext/iodine/fio_dict.h +90 -0
- data/ext/iodine/fio_hash_table.h +370 -0
- data/ext/iodine/fio_list.h +30 -3
- data/ext/iodine/http.c +169 -37
- data/ext/iodine/http.h +33 -10
- data/ext/iodine/http1.c +78 -42
- data/ext/iodine/http_request.c +6 -0
- data/ext/iodine/http_request.h +3 -0
- data/ext/iodine/http_response.c +43 -11
- data/ext/iodine/iodine.c +380 -0
- data/ext/iodine/iodine.h +62 -0
- data/ext/iodine/iodine_helpers.c +235 -0
- data/ext/iodine/iodine_helpers.h +13 -0
- data/ext/iodine/iodine_http.c +409 -241
- data/ext/iodine/iodine_http.h +7 -14
- data/ext/iodine/iodine_protocol.c +626 -0
- data/ext/iodine/iodine_protocol.h +13 -0
- data/ext/iodine/iodine_pubsub.c +646 -0
- data/ext/iodine/iodine_pubsub.h +27 -0
- data/ext/iodine/iodine_websockets.c +796 -0
- data/ext/iodine/iodine_websockets.h +19 -0
- data/ext/iodine/pubsub.c +544 -0
- data/ext/iodine/pubsub.h +215 -0
- data/ext/iodine/random.c +4 -4
- data/ext/iodine/rb-call.c +1 -5
- data/ext/iodine/rb-defer.c +3 -20
- data/ext/iodine/rb-rack-io.c +22 -22
- data/ext/iodine/rb-rack-io.h +3 -4
- data/ext/iodine/rb-registry.c +111 -118
- data/ext/iodine/redis_connection.c +277 -0
- data/ext/iodine/redis_connection.h +77 -0
- data/ext/iodine/redis_engine.c +398 -0
- data/ext/iodine/redis_engine.h +68 -0
- data/ext/iodine/resp.c +842 -0
- data/ext/iodine/resp.h +253 -0
- data/ext/iodine/sock.c +26 -12
- data/ext/iodine/sock.h +14 -3
- data/ext/iodine/spnlock.inc +19 -2
- data/ext/iodine/websockets.c +299 -11
- data/ext/iodine/websockets.h +159 -6
- data/lib/iodine.rb +104 -1
- data/lib/iodine/cli.rb +106 -0
- data/lib/iodine/monkeypatch.rb +40 -0
- data/lib/iodine/pubsub.rb +70 -0
- data/lib/iodine/version.rb +1 -1
- data/lib/iodine/websocket.rb +12 -0
- data/lib/rack/handler/iodine.rb +33 -7
- metadata +35 -7
- data/ext/iodine/iodine_core.c +0 -760
- data/ext/iodine/iodine_core.h +0 -79
- data/ext/iodine/iodine_websocket.c +0 -551
- data/ext/iodine/iodine_websocket.h +0 -22
- data/lib/iodine/http.rb +0 -4
@@ -0,0 +1,68 @@
|
|
1
|
+
/*
|
2
|
+
Copyright: Boaz segev, 2017
|
3
|
+
License: MIT except for any non-public-domain algorithms (none that I'm aware
|
4
|
+
of), which might be subject to their own licenses.
|
5
|
+
|
6
|
+
Feel free to copy, use and enjoy in accordance with to the license(s).
|
7
|
+
*/
|
8
|
+
#ifndef H_REDIS_ENGINE_H
|
9
|
+
/**
|
10
|
+
This is a simple, optimistic Redis engine that matches the requirements of
|
11
|
+
facil.io's pub/sub engine design.
|
12
|
+
|
13
|
+
The engine is optimistic, meanning the engine will never report a failed
|
14
|
+
subscription or publication... it will simply try until successful.
|
15
|
+
*/
|
16
|
+
#define H_REDIS_ENGINE_H
|
17
|
+
#include "pubsub.h"
|
18
|
+
#include "resp.h"
|
19
|
+
|
20
|
+
/** possible arguments for the `redis_engine_create` function call */
|
21
|
+
struct redis_engine_create_args {
|
22
|
+
/** Redis server's address */
|
23
|
+
const char *address;
|
24
|
+
/** Redis server's port */
|
25
|
+
const char *port;
|
26
|
+
/** Redis server's password, if any */
|
27
|
+
const char *auth;
|
28
|
+
/** Redis server's password length (if any). */
|
29
|
+
size_t auth_len;
|
30
|
+
/** A `ping` will be sent every `ping_interval` interval or inactivity. */
|
31
|
+
uint8_t ping_interval;
|
32
|
+
};
|
33
|
+
|
34
|
+
/**
|
35
|
+
See the {pubsub.h} file for documentation about engines.
|
36
|
+
|
37
|
+
The engine is active only after facil.io starts running.
|
38
|
+
|
39
|
+
A `ping` will be sent every `ping_interval` interval or inactivity. The default
|
40
|
+
value (0) will fallback to facil.io's maximum time of inactivity (5 minutes)
|
41
|
+
before polling on the connection's protocol.
|
42
|
+
|
43
|
+
function names speak for themselves ;-)
|
44
|
+
*/
|
45
|
+
pubsub_engine_s *redis_engine_create(struct redis_engine_create_args);
|
46
|
+
|
47
|
+
#define redis_engine_create(...) \
|
48
|
+
redis_engine_create((struct redis_engine_create_args){__VA_ARGS__})
|
49
|
+
|
50
|
+
/**
|
51
|
+
Sends a Redis message through the engine's connection. The response will be sent
|
52
|
+
back using the optional callback. `udata` is passed along untouched.
|
53
|
+
|
54
|
+
The message will be repeated endlessly until a response validates the fact that
|
55
|
+
it was sent (or the engine is destroyed).
|
56
|
+
*/
|
57
|
+
intptr_t redis_engine_send(pubsub_engine_s *engine, resp_object_s *data,
|
58
|
+
void (*callback)(pubsub_engine_s *e,
|
59
|
+
resp_object_s *reply, void *udata),
|
60
|
+
void *udata);
|
61
|
+
|
62
|
+
/**
|
63
|
+
See the {pubsub.h} file for documentation about engines.
|
64
|
+
|
65
|
+
function names speak for themselves ;-)
|
66
|
+
*/
|
67
|
+
void redis_engine_destroy(pubsub_engine_s *engine);
|
68
|
+
#endif
|
data/ext/iodine/resp.c
ADDED
@@ -0,0 +1,842 @@
|
|
1
|
+
/*
|
2
|
+
Copyright: Boaz Segev, 2017
|
3
|
+
License: MIT
|
4
|
+
|
5
|
+
Feel free to copy, use and enjoy according to the license provided.
|
6
|
+
|
7
|
+
Copyright refers to the parser, not the protocol.
|
8
|
+
*/
|
9
|
+
|
10
|
+
/* for atomic operations when reference counting: `spn_add` and `spn_sub` */
|
11
|
+
#include "spnlock.inc"
|
12
|
+
|
13
|
+
#include "resp.h"
|
14
|
+
#include <math.h>
|
15
|
+
#include <stdio.h>
|
16
|
+
#include <string.h>
|
17
|
+
|
18
|
+
/* *****************************************************************************
|
19
|
+
Error Reporting
|
20
|
+
***************************************************************************** */
|
21
|
+
#ifdef DEBUG
|
22
|
+
#define REPORT(...) fprintf(stderr, __VA_ARGS__);
|
23
|
+
#else
|
24
|
+
#define REPORT(...)
|
25
|
+
#endif
|
26
|
+
/* *****************************************************************************
|
27
|
+
The parser object
|
28
|
+
***************************************************************************** */
|
29
|
+
|
30
|
+
typedef struct resp_parser_s {
|
31
|
+
resp_object_s *obj;
|
32
|
+
size_t missing;
|
33
|
+
uint8_t *leftovers;
|
34
|
+
size_t llen;
|
35
|
+
unsigned has_first_object : 1;
|
36
|
+
unsigned exclude_pubsub : 1;
|
37
|
+
unsigned multiplex_pubsub : 1;
|
38
|
+
} resp_parser_s;
|
39
|
+
|
40
|
+
/* *****************************************************************************
|
41
|
+
Pub/Sub state and Extension Flags
|
42
|
+
***************************************************************************** */
|
43
|
+
|
44
|
+
/** Set the parsing mode to enable the experimental pub/sub duplexing. */
|
45
|
+
void resp_enable_duplex_pubsub(resp_parser_pt p) { p->multiplex_pubsub = 1; }
|
46
|
+
|
47
|
+
/* *****************************************************************************
|
48
|
+
Object management
|
49
|
+
+
|
50
|
+
Sometimes you just need a dirty stack.
|
51
|
+
***************************************************************************** */
|
52
|
+
|
53
|
+
typedef struct {
|
54
|
+
volatile uintptr_t ref;
|
55
|
+
resp_object_s *link;
|
56
|
+
} resp_objhead_s;
|
57
|
+
|
58
|
+
#define OBJHEAD(obj) (((resp_objhead_s *)(obj)) - 1)
|
59
|
+
|
60
|
+
static resp_object_s *resp_alloc_obj(enum resp_type_enum type, size_t length) {
|
61
|
+
resp_objhead_s *head = NULL;
|
62
|
+
switch (type) {
|
63
|
+
/* Fallthrough */
|
64
|
+
case RESP_ERR:
|
65
|
+
case RESP_STRING:
|
66
|
+
/* + 1 for NULL (safety) */
|
67
|
+
head = malloc(sizeof(*head) + sizeof(resp_string_s) + length + 1);
|
68
|
+
*head = (resp_objhead_s){.ref = 1};
|
69
|
+
resp_string_s *s = (void *)(head + 1);
|
70
|
+
*s = (resp_string_s){.type = type, .len = length};
|
71
|
+
return (void *)s;
|
72
|
+
break;
|
73
|
+
case RESP_NULL:
|
74
|
+
case RESP_OK:
|
75
|
+
head = malloc(sizeof(*head) + sizeof(resp_object_s));
|
76
|
+
*head = (resp_objhead_s){.ref = 1};
|
77
|
+
resp_object_s *ok = (void *)(head + 1);
|
78
|
+
*ok = (resp_object_s){.type = type};
|
79
|
+
return (void *)ok;
|
80
|
+
break;
|
81
|
+
case RESP_ARRAY:
|
82
|
+
case RESP_PUBSUB:
|
83
|
+
head = malloc(sizeof(*head) + sizeof(resp_array_s) +
|
84
|
+
(sizeof(resp_object_s *) * length));
|
85
|
+
*head = (resp_objhead_s){.ref = 1};
|
86
|
+
resp_array_s *ar = (void *)(head + 1);
|
87
|
+
*ar = (resp_array_s){.type = type, .len = length};
|
88
|
+
return (void *)ar;
|
89
|
+
break;
|
90
|
+
case RESP_NUMBER:
|
91
|
+
head = malloc(sizeof(*head) + sizeof(resp_number_s));
|
92
|
+
*head = (resp_objhead_s){.ref = 1};
|
93
|
+
resp_number_s *i = (void *)(head + 1);
|
94
|
+
*i = (resp_number_s){.type = RESP_NUMBER, .number = length};
|
95
|
+
return (void *)i;
|
96
|
+
break;
|
97
|
+
}
|
98
|
+
return NULL;
|
99
|
+
}
|
100
|
+
|
101
|
+
static int resp_dealloc_obj(resp_parser_pt p, resp_object_s *obj, void *arg) {
|
102
|
+
if (obj && !spn_sub(&OBJHEAD(obj)->ref, 1))
|
103
|
+
free(OBJHEAD(obj));
|
104
|
+
(void)arg;
|
105
|
+
(void)p;
|
106
|
+
return 0;
|
107
|
+
}
|
108
|
+
|
109
|
+
inline static resp_object_s *pop_obj(resp_object_s *obj) {
|
110
|
+
resp_object_s *ret = OBJHEAD(obj)->link;
|
111
|
+
if (ret) {
|
112
|
+
OBJHEAD(obj)->link = OBJHEAD(ret)->link;
|
113
|
+
OBJHEAD(ret)->link = NULL;
|
114
|
+
} else
|
115
|
+
OBJHEAD(obj)->link = NULL;
|
116
|
+
return ret;
|
117
|
+
}
|
118
|
+
|
119
|
+
inline static void push_obj(resp_object_s *dest, resp_object_s *obj) {
|
120
|
+
if (!dest)
|
121
|
+
return;
|
122
|
+
if (obj) {
|
123
|
+
OBJHEAD(obj)->link = OBJHEAD(dest)->link;
|
124
|
+
}
|
125
|
+
OBJHEAD(dest)->link = obj;
|
126
|
+
}
|
127
|
+
|
128
|
+
static int mock_obj_tks(resp_parser_pt p, resp_object_s *obj, void *arg) {
|
129
|
+
return 0;
|
130
|
+
(void)arg;
|
131
|
+
(void)p;
|
132
|
+
(void)obj;
|
133
|
+
}
|
134
|
+
|
135
|
+
/** Performs a task on each object. Protects from loop-backs. */
|
136
|
+
size_t resp_obj_each(resp_parser_pt p, resp_object_s *obj,
|
137
|
+
int (*task)(resp_parser_pt p, resp_object_s *obj,
|
138
|
+
void *arg),
|
139
|
+
void *arg) {
|
140
|
+
|
141
|
+
if (!obj)
|
142
|
+
return 0;
|
143
|
+
if (!task)
|
144
|
+
task = mock_obj_tks;
|
145
|
+
/* make sure the object isn't "dirty"*/
|
146
|
+
OBJHEAD(obj)->link = NULL;
|
147
|
+
/* a searchable store. */
|
148
|
+
resp_objhead_s performed[3] = {{.link = NULL}}; /* a memory lump. */
|
149
|
+
resp_object_s *past = (resp_object_s *)(performed + 1);
|
150
|
+
resp_object_s *next = (resp_object_s *)(performed + 2);
|
151
|
+
OBJHEAD(past)->link = NULL;
|
152
|
+
OBJHEAD(next)->link = NULL; /* a straight line */
|
153
|
+
push_obj(next, obj);
|
154
|
+
resp_object_s *err = NULL; /* might host an error object... if we need one. */
|
155
|
+
resp_object_s *tmp;
|
156
|
+
size_t count = 0;
|
157
|
+
obj = pop_obj(next);
|
158
|
+
while (obj) {
|
159
|
+
count++;
|
160
|
+
if (obj->type == RESP_ARRAY || obj->type == RESP_PUBSUB)
|
161
|
+
goto is_array;
|
162
|
+
|
163
|
+
perform:
|
164
|
+
/* in case the task is to free `obj`, "pop" the next object first */
|
165
|
+
tmp = obj;
|
166
|
+
obj = pop_obj(next);
|
167
|
+
if (task(p, tmp, arg) == -1)
|
168
|
+
break;
|
169
|
+
continue;
|
170
|
+
|
171
|
+
is_array:
|
172
|
+
/* test for loop-back. */
|
173
|
+
tmp = OBJHEAD(past)->link;
|
174
|
+
while (tmp) {
|
175
|
+
if (obj == tmp)
|
176
|
+
goto skip;
|
177
|
+
tmp = OBJHEAD(tmp)->link;
|
178
|
+
}
|
179
|
+
/* add current object to the loop back stack */
|
180
|
+
push_obj(past, obj);
|
181
|
+
|
182
|
+
/* Add all array items to the stack. */
|
183
|
+
resp_obj2arr(obj)->pos = resp_obj2arr(obj)->len;
|
184
|
+
while (resp_obj2arr(obj)->pos != 0) {
|
185
|
+
resp_obj2arr(obj)->pos--;
|
186
|
+
if (!resp_obj2arr(obj)
|
187
|
+
->array[resp_obj2arr(obj)->pos]) /* fill any holes...? */
|
188
|
+
resp_obj2arr(obj)->array[resp_obj2arr(obj)->pos] = resp_nil2obj();
|
189
|
+
push_obj(next, resp_obj2arr(obj)->array[resp_obj2arr(obj)->pos]);
|
190
|
+
}
|
191
|
+
goto perform;
|
192
|
+
|
193
|
+
skip:
|
194
|
+
if (task != resp_dealloc_obj) {
|
195
|
+
if (!err)
|
196
|
+
err =
|
197
|
+
resp_err2obj("ERR: infinit loopback detected. Can't process.", 46);
|
198
|
+
obj = err;
|
199
|
+
} else
|
200
|
+
obj = resp_err2obj("ERR: infinit loopback detected. Can't process.", 46);
|
201
|
+
goto perform;
|
202
|
+
}
|
203
|
+
|
204
|
+
/* cleaup */
|
205
|
+
if (task != resp_dealloc_obj) {
|
206
|
+
if (err)
|
207
|
+
resp_dealloc_obj(NULL, err, NULL);
|
208
|
+
while ((tmp = pop_obj(past)))
|
209
|
+
;
|
210
|
+
}
|
211
|
+
return count;
|
212
|
+
}
|
213
|
+
|
214
|
+
/* *****************************************************************************
|
215
|
+
Object API
|
216
|
+
***************************************************************************** */
|
217
|
+
|
218
|
+
/** Allocates an RESP NULL objcet. Remeber to free when done. */
|
219
|
+
resp_object_s *resp_nil2obj(void) { return resp_alloc_obj(RESP_NULL, 0); }
|
220
|
+
|
221
|
+
/** Allocates an RESP OK objcet. Remeber to free when done. */
|
222
|
+
resp_object_s *resp_OK2obj(void) { return resp_alloc_obj(RESP_OK, 0); }
|
223
|
+
|
224
|
+
/** Allocates an RESP Error objcet. Remeber to free when done. */
|
225
|
+
resp_object_s *resp_err2obj(const void *msg, size_t len) {
|
226
|
+
resp_string_s *o = (resp_string_s *)resp_alloc_obj(RESP_STRING, len);
|
227
|
+
memcpy(o->string, msg, len);
|
228
|
+
o->string[len] = 0;
|
229
|
+
return (void *)o;
|
230
|
+
}
|
231
|
+
|
232
|
+
/** Allocates an RESP Number objcet. Remeber to free when done. */
|
233
|
+
resp_object_s *resp_num2obj(uint64_t num) {
|
234
|
+
return resp_alloc_obj(RESP_NUMBER, num);
|
235
|
+
}
|
236
|
+
|
237
|
+
/** Allocates an RESP String objcet. Remeber to free when done. */
|
238
|
+
resp_object_s *resp_str2obj(const void *str, size_t len) {
|
239
|
+
resp_string_s *o = (resp_string_s *)resp_alloc_obj(RESP_STRING, len);
|
240
|
+
memcpy(o->string, str, len);
|
241
|
+
o->string[len] = 0;
|
242
|
+
return (void *)o;
|
243
|
+
}
|
244
|
+
|
245
|
+
/** Allocates an RESP Array objcet. Remeber to free when done. */
|
246
|
+
resp_object_s *resp_arr2obj(int argc, resp_object_s *argv[]) {
|
247
|
+
if (argc < 0)
|
248
|
+
return NULL;
|
249
|
+
resp_array_s *o = (resp_array_s *)resp_alloc_obj(RESP_ARRAY, argc);
|
250
|
+
if (argv) {
|
251
|
+
for (int i = 0; i < argc; i++) {
|
252
|
+
o->array[i] = (resp_object_s *)argv[i];
|
253
|
+
}
|
254
|
+
}
|
255
|
+
return (void *)o;
|
256
|
+
}
|
257
|
+
|
258
|
+
/** frees an object returned from the parser. */
|
259
|
+
void resp_free_object(resp_object_s *obj) {
|
260
|
+
resp_obj_each(NULL, obj, resp_dealloc_obj, NULL);
|
261
|
+
}
|
262
|
+
|
263
|
+
static int resp_dup_obj_task(resp_parser_pt p, resp_object_s *obj, void *arg) {
|
264
|
+
spn_add(&OBJHEAD(obj)->ref, 1);
|
265
|
+
return 0;
|
266
|
+
(void)p;
|
267
|
+
(void)arg;
|
268
|
+
}
|
269
|
+
|
270
|
+
/** Duplicates an object by increasing it's reference count. */
|
271
|
+
resp_object_s *resp_dup_object(resp_object_s *obj) {
|
272
|
+
resp_obj_each(NULL, obj, resp_dup_obj_task, NULL);
|
273
|
+
return obj;
|
274
|
+
}
|
275
|
+
|
276
|
+
/* *****************************************************************************
|
277
|
+
Formatter (RESP => Memory Buffer)
|
278
|
+
***************************************************************************** */
|
279
|
+
struct resp_format_s {
|
280
|
+
uint8_t *dest;
|
281
|
+
size_t *size;
|
282
|
+
size_t limit;
|
283
|
+
uintptr_t multiplex_pubsub;
|
284
|
+
int err;
|
285
|
+
};
|
286
|
+
|
287
|
+
static int resp_format_task(resp_parser_pt p, resp_object_s *obj, void *s_) {
|
288
|
+
struct resp_format_s *s = s_;
|
289
|
+
|
290
|
+
#define safe_write_eol() \
|
291
|
+
if ((*s->size += 2) <= s->limit) { \
|
292
|
+
*s->dest++ = '\r'; \
|
293
|
+
*s->dest++ = '\n'; \
|
294
|
+
}
|
295
|
+
#define safe_write1(data) \
|
296
|
+
if (++(*s->size) <= s->limit) { \
|
297
|
+
*s->dest++ = (data); \
|
298
|
+
}
|
299
|
+
#define safe_write2(data, len) \
|
300
|
+
do { \
|
301
|
+
*s->size += (len); \
|
302
|
+
if (*s->size <= s->limit) { \
|
303
|
+
memcpy(s->dest, (data), (len)); \
|
304
|
+
s->dest += (len); \
|
305
|
+
} \
|
306
|
+
} while (0)
|
307
|
+
#define safe_write_i(i) \
|
308
|
+
do { \
|
309
|
+
int64_t t2 = (i); \
|
310
|
+
if ((t2) < 0) { \
|
311
|
+
safe_write1('-'); \
|
312
|
+
t2 = (t2 * -1); \
|
313
|
+
} \
|
314
|
+
size_t len = t2 ? (((size_t)log10((double)(t2))) + 1) : 1; \
|
315
|
+
*s->size += (len); \
|
316
|
+
if (*s->size <= s->limit) { \
|
317
|
+
int64_t t1 = len; \
|
318
|
+
int64_t t3 = t2 / 10; \
|
319
|
+
while (t1--) { \
|
320
|
+
s->dest[t1] = '0' + (t2 - (t3 * 10)); \
|
321
|
+
t2 = t3; \
|
322
|
+
t3 = t3 / 10; \
|
323
|
+
} \
|
324
|
+
s->dest += len; \
|
325
|
+
} \
|
326
|
+
} while (0)
|
327
|
+
|
328
|
+
switch (obj->type) {
|
329
|
+
case RESP_ERR:
|
330
|
+
safe_write1('-');
|
331
|
+
safe_write2((resp_obj2str(obj)->string), (resp_obj2str(obj)->len));
|
332
|
+
safe_write_eol();
|
333
|
+
break;
|
334
|
+
case RESP_NULL:
|
335
|
+
safe_write2("$-1\r\n", (resp_obj2str(obj)->len));
|
336
|
+
break;
|
337
|
+
case RESP_OK:
|
338
|
+
safe_write2("+OK\r\n", 5);
|
339
|
+
break;
|
340
|
+
case RESP_ARRAY:
|
341
|
+
case RESP_PUBSUB:
|
342
|
+
safe_write1('*');
|
343
|
+
safe_write_i(resp_obj2arr(obj)->len);
|
344
|
+
safe_write_eol();
|
345
|
+
break;
|
346
|
+
case RESP_STRING:
|
347
|
+
safe_write1('$');
|
348
|
+
safe_write_i(s->multiplex_pubsub ? (resp_obj2str(obj)->len + 1)
|
349
|
+
: resp_obj2str(obj)->len);
|
350
|
+
safe_write_eol();
|
351
|
+
if (s->multiplex_pubsub) {
|
352
|
+
s->multiplex_pubsub = 0;
|
353
|
+
safe_write1('+');
|
354
|
+
}
|
355
|
+
safe_write2((resp_obj2str(obj)->string), (resp_obj2str(obj)->len));
|
356
|
+
safe_write_eol();
|
357
|
+
break;
|
358
|
+
case RESP_NUMBER:
|
359
|
+
safe_write1(':');
|
360
|
+
safe_write_i((resp_obj2num(obj)->number));
|
361
|
+
safe_write_eol();
|
362
|
+
break;
|
363
|
+
}
|
364
|
+
return 0;
|
365
|
+
(void)p;
|
366
|
+
}
|
367
|
+
#undef safe_write_eol
|
368
|
+
#undef safe_write1
|
369
|
+
#undef safe_write2
|
370
|
+
#undef safe_write_i
|
371
|
+
|
372
|
+
int resp_format(resp_parser_pt p, uint8_t *dest, size_t *size,
|
373
|
+
resp_object_s *obj) {
|
374
|
+
struct resp_format_s arg = {
|
375
|
+
.dest = dest,
|
376
|
+
.size = size,
|
377
|
+
.limit = *size,
|
378
|
+
.multiplex_pubsub =
|
379
|
+
(p && p->multiplex_pubsub && obj->type == RESP_ARRAY &&
|
380
|
+
((resp_array_s *)obj)->len &&
|
381
|
+
((resp_array_s *)obj)->array[0]->type == RESP_STRING &&
|
382
|
+
((((resp_string_s *)((resp_array_s *)obj)->array[0])->string[0] ==
|
383
|
+
'm') ||
|
384
|
+
(((resp_string_s *)((resp_array_s *)obj)->array[0])->string[0] ==
|
385
|
+
'M') ||
|
386
|
+
(((resp_string_s *)((resp_array_s *)obj)->array[0])->string[0] ==
|
387
|
+
'+')))};
|
388
|
+
*size = 0;
|
389
|
+
resp_obj_each(p, obj, resp_format_task, &arg);
|
390
|
+
if (*arg.size < arg.limit) {
|
391
|
+
*arg.dest = 0;
|
392
|
+
return 0;
|
393
|
+
}
|
394
|
+
if (*arg.size == arg.limit)
|
395
|
+
return 0;
|
396
|
+
return -1;
|
397
|
+
}
|
398
|
+
|
399
|
+
/* *****************************************************************************
|
400
|
+
Parser (Memory Buffer => RESP)
|
401
|
+
***************************************************************************** */
|
402
|
+
|
403
|
+
/** create the parser */
|
404
|
+
resp_parser_pt resp_parser_new(void) {
|
405
|
+
resp_parser_s *p = malloc(sizeof(*p));
|
406
|
+
*p = (resp_parser_s){.obj = NULL};
|
407
|
+
return p;
|
408
|
+
}
|
409
|
+
|
410
|
+
static inline void reset_parser(resp_parser_pt p) {
|
411
|
+
resp_object_s *tmp;
|
412
|
+
while ((tmp = p->obj)) {
|
413
|
+
p->obj = OBJHEAD(p->obj)->link;
|
414
|
+
resp_free_object(tmp);
|
415
|
+
}
|
416
|
+
if (p->leftovers)
|
417
|
+
free(p->leftovers);
|
418
|
+
if (p->obj)
|
419
|
+
resp_free_object(p->obj);
|
420
|
+
*p = (resp_parser_s){.obj = NULL, .multiplex_pubsub = p->multiplex_pubsub};
|
421
|
+
}
|
422
|
+
|
423
|
+
/** free the parser and it's resources. */
|
424
|
+
void resp_parser_destroy(resp_parser_pt p) {
|
425
|
+
reset_parser(p);
|
426
|
+
free(p);
|
427
|
+
}
|
428
|
+
|
429
|
+
void resp_parser_clear(resp_parser_pt p) {
|
430
|
+
reset_parser(p);
|
431
|
+
*p = (resp_parser_s){.obj = NULL};
|
432
|
+
}
|
433
|
+
|
434
|
+
/**
|
435
|
+
* Feed the parser with data.
|
436
|
+
*
|
437
|
+
* Returns any fully parsed object / reply (often an array, but not always) or
|
438
|
+
* NULL (needs more data / error).
|
439
|
+
*
|
440
|
+
* If an error occurs, the `pos` pointer is set to -1, otherwise it's updated
|
441
|
+
* with the amount of data consumed.
|
442
|
+
*
|
443
|
+
* Partial consumption is possible when multiple replys were available in the
|
444
|
+
* buffer. Otherwise the parser will consume the whole of the buffer.
|
445
|
+
*
|
446
|
+
*/
|
447
|
+
resp_object_s *resp_parser_feed(resp_parser_pt p, uint8_t *buf, size_t *len) {
|
448
|
+
resp_object_s *tmp;
|
449
|
+
uint8_t *pos = buf;
|
450
|
+
uint8_t *eol = NULL;
|
451
|
+
uint8_t *end = pos + (*len);
|
452
|
+
int64_t num;
|
453
|
+
uint8_t flag;
|
454
|
+
restart:
|
455
|
+
|
456
|
+
// while (pos < end && (*pos == '\r' || *pos == '\n'))
|
457
|
+
// pos++; /* consume empty EOL markers. */
|
458
|
+
|
459
|
+
if (p->missing && p->obj->type == RESP_STRING) {
|
460
|
+
/* test for pub/sub duplexing extension */
|
461
|
+
if (!p->has_first_object && p->multiplex_pubsub && pos[0] == '+' &&
|
462
|
+
(pos[1] == '+' || pos[1] == 'm' || pos[1] == 'M')) {
|
463
|
+
pos++;
|
464
|
+
p->exclude_pubsub = 1;
|
465
|
+
p->missing--;
|
466
|
+
}
|
467
|
+
p->has_first_object = 1;
|
468
|
+
/* just fill the string with missing bytes */
|
469
|
+
if (p->missing > (size_t)(end - pos)) {
|
470
|
+
memcpy(resp_obj2str(p->obj)->string + resp_obj2str(p->obj)->len -
|
471
|
+
p->missing,
|
472
|
+
pos, (size_t)(end - pos));
|
473
|
+
p->missing -= (size_t)(end - pos);
|
474
|
+
pos = end;
|
475
|
+
goto finish;
|
476
|
+
} else {
|
477
|
+
memcpy(resp_obj2str(p->obj)->string + resp_obj2str(p->obj)->len -
|
478
|
+
p->missing,
|
479
|
+
pos, p->missing);
|
480
|
+
resp_obj2str(p->obj)->string[resp_obj2str(p->obj)->len] = 0; /* set NUL */
|
481
|
+
pos += p->missing + 2; /* eat the EOL */
|
482
|
+
/* set state to equal a freash, complete object */
|
483
|
+
p->missing = 0;
|
484
|
+
tmp = p->obj;
|
485
|
+
p->obj = OBJHEAD(p->obj)->link;
|
486
|
+
goto review;
|
487
|
+
}
|
488
|
+
}
|
489
|
+
|
490
|
+
/* seek EOL */
|
491
|
+
eol = pos;
|
492
|
+
while (eol < end - 1) {
|
493
|
+
if (eol[0] == '\r' && eol[1] == '\n')
|
494
|
+
goto found_eol;
|
495
|
+
eol++;
|
496
|
+
}
|
497
|
+
|
498
|
+
/* no EOL, we can't parse. */
|
499
|
+
if (p->llen + (end - pos) >=
|
500
|
+
131072) { /* IMHO: simple objects are smaller than 128Kib. */
|
501
|
+
REPORT("ERROR: (RESP parser) single line object too long. "
|
502
|
+
"128Kib limit for simple strings, numbers exc'.\n");
|
503
|
+
eol = NULL;
|
504
|
+
goto error;
|
505
|
+
}
|
506
|
+
{
|
507
|
+
void *tmp = realloc(p->leftovers, p->llen + (end - pos));
|
508
|
+
if (!tmp) {
|
509
|
+
fprintf(stderr, "ERROR: (RESP parser) Couldn't allocate memory.\n");
|
510
|
+
goto error;
|
511
|
+
}
|
512
|
+
p->leftovers = (void *)tmp;
|
513
|
+
memcpy(p->leftovers + p->llen, pos, end - pos);
|
514
|
+
p->llen += (end - pos);
|
515
|
+
}
|
516
|
+
goto finish;
|
517
|
+
|
518
|
+
found_eol:
|
519
|
+
|
520
|
+
*eol = 0; /* mark with NUL */
|
521
|
+
|
522
|
+
num = eol - pos - 1;
|
523
|
+
/* route `pos` to p->leftovers, if data was fragmented. */
|
524
|
+
if (p->leftovers) {
|
525
|
+
void *tmp = realloc(p->leftovers, p->llen + num + 1);
|
526
|
+
if (!tmp) {
|
527
|
+
REPORT("ERROR: (RESP parser) Couldn't allocate memory.\n");
|
528
|
+
goto error;
|
529
|
+
}
|
530
|
+
p->leftovers = (void *)tmp;
|
531
|
+
memcpy(p->leftovers + p->llen, pos, num + 1); /* copy the NUL byte. */
|
532
|
+
p->llen += (end - pos);
|
533
|
+
pos = p->leftovers;
|
534
|
+
}
|
535
|
+
|
536
|
+
/* ** let's actually parse something ** put new objects in `tmp` ** */
|
537
|
+
switch (*pos++) {
|
538
|
+
case '*': /* Array */
|
539
|
+
num = 0;
|
540
|
+
while (*pos) {
|
541
|
+
if (*pos < '0' || *pos > '9')
|
542
|
+
goto error;
|
543
|
+
num = (num * 10) + (*pos - '0');
|
544
|
+
pos++;
|
545
|
+
}
|
546
|
+
tmp = resp_alloc_obj(RESP_ARRAY, num);
|
547
|
+
p->missing = num;
|
548
|
+
break;
|
549
|
+
|
550
|
+
case '$': /* String */
|
551
|
+
if (*pos == '-') {
|
552
|
+
if (pos[1] == '1') {
|
553
|
+
/* NULL Object */
|
554
|
+
tmp = resp_alloc_obj(RESP_NULL, -1);
|
555
|
+
p->missing = 0;
|
556
|
+
} else {
|
557
|
+
REPORT("ERROR: (RESP parser) Bulk String input error.\n");
|
558
|
+
goto error;
|
559
|
+
}
|
560
|
+
} else {
|
561
|
+
num = 0;
|
562
|
+
while (*pos) {
|
563
|
+
if (*pos < '0' || *pos > '9') {
|
564
|
+
REPORT("ERROR: (RESP parser) Bulk String length error.\n");
|
565
|
+
goto error;
|
566
|
+
}
|
567
|
+
num = (num * 10) + (*pos - '0');
|
568
|
+
pos++;
|
569
|
+
}
|
570
|
+
tmp = resp_alloc_obj(RESP_STRING, num);
|
571
|
+
p->missing = num;
|
572
|
+
if (!num) {
|
573
|
+
*eol = '\r';
|
574
|
+
eol += 2;
|
575
|
+
}
|
576
|
+
}
|
577
|
+
break;
|
578
|
+
|
579
|
+
case '+': /* Simple String */
|
580
|
+
case '-': /* Error String */
|
581
|
+
p->has_first_object = 1;
|
582
|
+
if (num == 2 && pos[0] == 'O' && pos[1] == 'K') {
|
583
|
+
tmp = resp_alloc_obj(RESP_OK, 0);
|
584
|
+
} else {
|
585
|
+
tmp = resp_alloc_obj((pos[-1] == '+' ? RESP_STRING : RESP_ERR), num);
|
586
|
+
memcpy(((resp_string_s *)tmp)->string, pos, num + 1); /* copy NUL */
|
587
|
+
}
|
588
|
+
p->missing = 0;
|
589
|
+
break;
|
590
|
+
|
591
|
+
case ':': /* number */
|
592
|
+
p->has_first_object = 1;
|
593
|
+
num = 0;
|
594
|
+
flag = 0;
|
595
|
+
if (*pos == '-') {
|
596
|
+
flag = 1;
|
597
|
+
pos++;
|
598
|
+
}
|
599
|
+
while (*pos) {
|
600
|
+
if (*pos < '0' || *pos > '9') {
|
601
|
+
REPORT("ERROR: (RESP parser) input error.\n");
|
602
|
+
goto error;
|
603
|
+
}
|
604
|
+
num = (num * 10) + (*pos - '0');
|
605
|
+
pos++;
|
606
|
+
}
|
607
|
+
if (flag)
|
608
|
+
num = num * -1;
|
609
|
+
tmp = resp_alloc_obj(RESP_NUMBER, num);
|
610
|
+
p->missing = 0;
|
611
|
+
break;
|
612
|
+
default:
|
613
|
+
REPORT("ERROR: (RESP Parser) input prefix unknown\n");
|
614
|
+
goto error;
|
615
|
+
}
|
616
|
+
/* replace parsing marker */
|
617
|
+
pos = eol + 2;
|
618
|
+
/* un-effect the EOL */
|
619
|
+
*eol = '\r';
|
620
|
+
eol = NULL;
|
621
|
+
|
622
|
+
/* clear the buffer used to handle fragmented transmissions. */
|
623
|
+
if (p->leftovers) {
|
624
|
+
free(p->leftovers);
|
625
|
+
p->leftovers = NULL;
|
626
|
+
p->llen = 0;
|
627
|
+
}
|
628
|
+
|
629
|
+
review:
|
630
|
+
/* handle object rotation and nesting */
|
631
|
+
if (p->missing) {
|
632
|
+
/* tmp missing data: link and step into new object (nesting objects) */
|
633
|
+
OBJHEAD(tmp)->link = p->obj;
|
634
|
+
p->obj = tmp;
|
635
|
+
tmp = NULL;
|
636
|
+
goto restart;
|
637
|
+
} else if (p->obj) {
|
638
|
+
if (p->obj->type != RESP_ARRAY) {
|
639
|
+
/* Nesting of objects can only be performed by RESP_ARRAY objects. */
|
640
|
+
fprintf(stderr, "ERROR: (RESP Parser) internal error - "
|
641
|
+
"objects can only be nested within arrays.\n");
|
642
|
+
goto error;
|
643
|
+
}
|
644
|
+
resp_obj2arr(p->obj)->array[resp_obj2arr(p->obj)->pos++] = tmp;
|
645
|
+
p->missing = resp_obj2arr(p->obj)->len - resp_obj2arr(p->obj)->pos;
|
646
|
+
if (p->missing)
|
647
|
+
goto restart; /* collect more objects. */
|
648
|
+
/* un-nest */
|
649
|
+
tmp = p->obj;
|
650
|
+
p->obj = OBJHEAD(p->obj)->link;
|
651
|
+
goto review;
|
652
|
+
}
|
653
|
+
|
654
|
+
/* tmp now holds the top-most object, it's missing no data, we're done */
|
655
|
+
|
656
|
+
/* test for pub/sub semantics */
|
657
|
+
if (!p->exclude_pubsub) {
|
658
|
+
if (tmp->type == RESP_ARRAY && resp_obj2arr(tmp)->len == 3 &&
|
659
|
+
resp_obj2arr(tmp)->array[0]->type == RESP_STRING &&
|
660
|
+
resp_obj2arr(tmp)->array[1]->type == RESP_STRING &&
|
661
|
+
resp_obj2arr(tmp)->array[2]->type == RESP_STRING &&
|
662
|
+
resp_obj2str(resp_obj2arr(tmp)->array[0])->len == 7 &&
|
663
|
+
!memcmp("message", resp_obj2str(resp_obj2arr(tmp)->array[0])->string,
|
664
|
+
7)) {
|
665
|
+
/* PUB / SUB */
|
666
|
+
tmp->type = RESP_PUBSUB;
|
667
|
+
}
|
668
|
+
}
|
669
|
+
/* reset the parser, keeping it's state. */
|
670
|
+
reset_parser(p);
|
671
|
+
/* report how much the parser actually "ate" */
|
672
|
+
*len = pos - buf;
|
673
|
+
/* return the result. */
|
674
|
+
return tmp;
|
675
|
+
|
676
|
+
error:
|
677
|
+
reset_parser(p);
|
678
|
+
*len = 0;
|
679
|
+
if (eol)
|
680
|
+
*eol = '\r';
|
681
|
+
|
682
|
+
finish:
|
683
|
+
return NULL;
|
684
|
+
}
|
685
|
+
|
686
|
+
/* *****************************************************************************
|
687
|
+
Tests
|
688
|
+
***************************************************************************** */
|
689
|
+
#ifdef DEBUG
|
690
|
+
#include <inttypes.h>
|
691
|
+
#include <stdint.h>
|
692
|
+
void resp_test(void) {
|
693
|
+
uint8_t b_null[] = "$-1\r\n";
|
694
|
+
uint8_t b_ok[] = "+OK\r\n";
|
695
|
+
uint8_t b_num[] = ":13\r\n";
|
696
|
+
uint8_t b_neg_num[] = ":-13\r\n";
|
697
|
+
uint8_t b_err[] = "-ERR: or not :-)\r\n";
|
698
|
+
uint8_t b_str[] = "$19\r\nthis is a string :)\r\n";
|
699
|
+
uint8_t b_longer[] = "*8\r\n"
|
700
|
+
":1\r\n"
|
701
|
+
":2\r\n"
|
702
|
+
":3\r\n"
|
703
|
+
":4\r\n"
|
704
|
+
":-5794\r\n"
|
705
|
+
"$6\r\n"
|
706
|
+
"foobar\r\n"
|
707
|
+
"$6\r\n"
|
708
|
+
"barfoo\r\n"
|
709
|
+
":4\r\n";
|
710
|
+
resp_parser_pt parser = resp_parser_new();
|
711
|
+
size_t len;
|
712
|
+
|
713
|
+
resp_object_s *obj;
|
714
|
+
|
715
|
+
fprintf(stderr, "* OBJHEAD test %s\n",
|
716
|
+
(OBJHEAD(((resp_objhead_s *)NULL) + 1) == NULL) ? "passed"
|
717
|
+
: "FAILED");
|
718
|
+
|
719
|
+
{
|
720
|
+
obj = resp_OK2obj();
|
721
|
+
resp_object_s *tmp = resp_nil2obj();
|
722
|
+
push_obj(obj, tmp);
|
723
|
+
fprintf(stderr, "* Push/Pop test: %s\n",
|
724
|
+
(pop_obj(obj) == tmp && pop_obj(obj) == NULL) ? "ok." : "FAILED!");
|
725
|
+
resp_free_object(obj);
|
726
|
+
resp_free_object(tmp);
|
727
|
+
}
|
728
|
+
|
729
|
+
len = sizeof(b_null) - 1;
|
730
|
+
obj = resp_parser_feed(parser, b_null, &len);
|
731
|
+
if (obj && obj->type == RESP_NULL) {
|
732
|
+
fprintf(stderr, "* NULL recognized, pos == %lu / %lu\n", len,
|
733
|
+
sizeof(b_null) - 1);
|
734
|
+
resp_free_object(obj);
|
735
|
+
} else
|
736
|
+
fprintf(stderr, "* NULL FAILED\n");
|
737
|
+
|
738
|
+
len = sizeof(b_ok) - 1;
|
739
|
+
obj = resp_parser_feed(parser, b_ok, &len);
|
740
|
+
if (obj && obj->type == RESP_OK) {
|
741
|
+
fprintf(stderr, "* OK recognized\n");
|
742
|
+
resp_free_object(obj);
|
743
|
+
} else
|
744
|
+
fprintf(stderr, "* OK FAILED\n");
|
745
|
+
|
746
|
+
len = sizeof(b_err) - 1;
|
747
|
+
obj = resp_parser_feed(parser, b_err, &len);
|
748
|
+
if (obj && obj->type == RESP_ERR) {
|
749
|
+
fprintf(stderr, "* ERR / Simple String recognized: %s\n",
|
750
|
+
resp_obj2str(obj)->string);
|
751
|
+
resp_free_object(obj);
|
752
|
+
} else
|
753
|
+
fprintf(stderr, "* ERR / Simple String FAILED (type %d)\n",
|
754
|
+
obj ? obj->type : -1);
|
755
|
+
|
756
|
+
len = sizeof(b_num) - 1;
|
757
|
+
obj = resp_parser_feed(parser, b_num, &len);
|
758
|
+
if (obj && obj->type == RESP_NUMBER) {
|
759
|
+
fprintf(stderr, "* Number recognized: %lld\n", resp_obj2num(obj)->number);
|
760
|
+
resp_free_object(obj);
|
761
|
+
} else
|
762
|
+
fprintf(stderr, "* Number FAILED\n");
|
763
|
+
|
764
|
+
len = sizeof(b_neg_num) - 1;
|
765
|
+
obj = resp_parser_feed(parser, b_neg_num, &len);
|
766
|
+
if (obj && obj->type == RESP_NUMBER) {
|
767
|
+
fprintf(stderr, "* Negative Number recognized: %lld\n",
|
768
|
+
resp_obj2num(obj)->number);
|
769
|
+
resp_free_object(obj);
|
770
|
+
} else
|
771
|
+
fprintf(stderr, "* Negative Number FAILED\n");
|
772
|
+
|
773
|
+
len = sizeof(b_str) - 1;
|
774
|
+
obj = resp_parser_feed(parser, b_str, &len);
|
775
|
+
if (obj && obj->type == RESP_STRING) {
|
776
|
+
fprintf(stderr, "* String recognized: %s\n", resp_obj2str(obj)->string);
|
777
|
+
resp_free_object(obj);
|
778
|
+
} else
|
779
|
+
fprintf(stderr, "* String FAILED\n");
|
780
|
+
|
781
|
+
{
|
782
|
+
uint8_t empty_str[] = "$0\r\n\r\n";
|
783
|
+
len = sizeof(empty_str) - 1;
|
784
|
+
obj = resp_parser_feed(parser, empty_str, &len);
|
785
|
+
if (obj && obj->type == RESP_STRING) {
|
786
|
+
fprintf(stderr, "* Empty String recognized: %s\n",
|
787
|
+
resp_obj2str(obj)->string);
|
788
|
+
resp_free_object(obj);
|
789
|
+
} else
|
790
|
+
fprintf(stderr, "* Empty String FAILED\n");
|
791
|
+
}
|
792
|
+
len = sizeof(b_longer) - 1;
|
793
|
+
obj = resp_parser_feed(parser, b_longer, &len);
|
794
|
+
if (obj && obj->type == RESP_ARRAY) {
|
795
|
+
fprintf(stderr, "* Array head recognized: (%lu)\n", resp_obj2arr(obj)->len);
|
796
|
+
resp_object_s *tmp;
|
797
|
+
for (size_t i = 0; i < resp_obj2arr(obj)->len; i++) {
|
798
|
+
tmp = resp_obj2arr(obj)->array[i];
|
799
|
+
if (resp_obj2arr(tmp)) {
|
800
|
+
fprintf(stderr, "Item %lu is an .... array?!\n", i);
|
801
|
+
}
|
802
|
+
if (resp_obj2str(tmp)) {
|
803
|
+
fprintf(stderr, "Item %lu is a String %s\n", i,
|
804
|
+
resp_obj2str(tmp)->string);
|
805
|
+
}
|
806
|
+
if (resp_obj2num(tmp)) {
|
807
|
+
fprintf(stderr, "Item %lu is a Number %" PRIi64 "\n", i,
|
808
|
+
resp_obj2num(tmp)->number);
|
809
|
+
}
|
810
|
+
}
|
811
|
+
{
|
812
|
+
fprintf(stderr, "found %lu objects\n",
|
813
|
+
resp_obj_each(NULL, obj, NULL, NULL));
|
814
|
+
uint8_t buff[128] = {0};
|
815
|
+
size_t len = 127;
|
816
|
+
resp_format(NULL, buff, &len, obj);
|
817
|
+
fprintf(stderr,
|
818
|
+
"* In RESP format, it should take %lu bytes like so:\n%s\n", len,
|
819
|
+
buff);
|
820
|
+
}
|
821
|
+
resp_free_object(obj);
|
822
|
+
} else {
|
823
|
+
fprintf(stderr, "* Array FAILED (type == %d)\n", obj ? obj->type : -1);
|
824
|
+
}
|
825
|
+
{
|
826
|
+
// uint8_t buff[128] = {0};
|
827
|
+
// size_t len = 128;
|
828
|
+
// resp_object_s *arr1 = resp_arr2obj(1, NULL);
|
829
|
+
// resp_object_s *arr2 = resp_arr2obj(1, NULL);
|
830
|
+
// resp_obj2arr(arr1)->array[0] = resp_dup_object(arr2);
|
831
|
+
// resp_obj2arr(arr2)->array[0] = resp_dup_object(arr1);
|
832
|
+
// resp_format(NULL, buff, &len, arr1);
|
833
|
+
// fprintf(stderr, "* Loopback test:\n%s\n", buff);
|
834
|
+
// resp_free_object(arr1);
|
835
|
+
// resp_free_object(arr2);
|
836
|
+
}
|
837
|
+
|
838
|
+
resp_parser_destroy(parser);
|
839
|
+
return;
|
840
|
+
}
|
841
|
+
|
842
|
+
#endif
|