command parser: Add support for the GETVAL command.
[collectd.git] / src / utils_cmds.c
1 /**
2  * collectd - src/utils_cmds.c
3  * Copyright (C) 2008       Florian Forster
4  * Copyright (C) 2016       Sebastian 'tokkee' Harl
5  *
6  * Permission is hereby granted, free of charge, to any person obtaining a
7  * copy of this software and associated documentation files (the "Software"),
8  * to deal in the Software without restriction, including without limitation
9  * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10  * and/or sell copies of the Software, and to permit persons to whom the
11  * Software is furnished to do so, subject to the following conditions:
12  *
13  * The above copyright notice and this permission notice shall be included in
14  * all copies or substantial portions of the Software.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22  * DEALINGS IN THE SOFTWARE.
23  *
24  * Authors:
25  *   Florian octo Forster <octo at collectd.org>
26  *   Sebastian 'tokkee' Harl <sh at tokkee.org>
27  **/
28
29 #include "utils_cmds.h"
30 #include "utils_cmd_flush.h"
31 #include "utils_cmd_getval.h"
32 #include "utils_cmd_listval.h"
33 #include "utils_cmd_putval.h"
34 #include "utils_parse_option.h"
35 #include "daemon/common.h"
36
37 #include <stdbool.h>
38 #include <string.h>
39
40 /*
41  * private helper functions
42  */
43
44 static cmd_status_t cmd_split (char *buffer,
45                 size_t *ret_len, char ***ret_fields,
46                 cmd_error_handler_t *err)
47 {
48         char *string, *field;
49         bool in_field, in_quotes;
50
51         size_t estimate, len;
52         char **fields;
53
54         estimate = 0;
55         in_field = false;
56         for (string = buffer; *string != '\0'; ++string)
57         {
58                 /* Make a quick worst-case estimate of the number of fields by
59                  * counting spaces and ignoring quotation marks. */
60                 if (!isspace ((int)*string))
61                 {
62                         if (!in_field)
63                         {
64                                 estimate++;
65                                 in_field = true;
66                         }
67                 }
68                 else
69                 {
70                         in_field = false;
71                 }
72         }
73
74         /* fields will be NULL-terminated */
75         fields = malloc ((estimate + 1) * sizeof (*fields));
76         if (fields == NULL) {
77                 cmd_error (CMD_ERROR, err, "malloc failed.");
78                 return (CMD_ERROR);
79         }
80
81 #define END_FIELD() \
82         do { \
83                 *field = '\0'; \
84                 field = NULL; \
85                 in_field = false; \
86         } while (0)
87 #define NEW_FIELD() \
88         do { \
89                 field = string; \
90                 in_field = true; \
91                 assert (len < estimate); \
92                 fields[len] = field; \
93                 field++; \
94                 len++; \
95         } while (0)
96
97         len = 0;
98         field = NULL;
99         in_field = false;
100         in_quotes = false;
101         for (string = buffer; *string != '\0'; string++)
102         {
103                 if (isspace ((int)string[0]))
104                 {
105                         if (! in_quotes)
106                         {
107                                 if (in_field)
108                                         END_FIELD ();
109
110                                 /* skip space */
111                                 continue;
112                         }
113                 }
114                 else if (string[0] == '"')
115                 {
116                         /* Note: Two consecutive quoted fields not separated by space are
117                          * treated as different fields. This is the collectd 5.x behavior
118                          * around splitting fields. */
119
120                         if (in_quotes)
121                         {
122                                 /* end of quoted field */
123                                 if (! in_field) /* empty quoted string */
124                                         NEW_FIELD ();
125                                 END_FIELD ();
126                                 in_quotes = false;
127                                 continue;
128                         }
129
130                         in_quotes = true;
131                         /* if (! in_field): add new field on next iteration
132                          * else: quoted string following an unquoted string (one field)
133                          * in either case: skip quotation mark */
134                         continue;
135                 }
136                 else if ((string[0] == '\\') && in_quotes)
137                 {
138                         /* Outside of quotes, a backslash is a regular character (mostly
139                          * for backward compatibility). */
140
141                         if (string[1] == '\0')
142                         {
143                                 free (fields);
144                                 cmd_error (CMD_PARSE_ERROR, err,
145                                                 "Backslash at end of string.");
146                                 return (CMD_PARSE_ERROR);
147                         }
148
149                         /* un-escape the next character; skip backslash */
150                         string++;
151                 }
152
153                 if (! in_field)
154                         NEW_FIELD ();
155                 else {
156                         *field = string[0];
157                         field++;
158                 }
159         }
160
161         if (in_quotes)
162         {
163                 free (fields);
164                 cmd_error (CMD_PARSE_ERROR, err, "Unterminated quoted string.");
165                 return (CMD_PARSE_ERROR);
166         }
167
168 #undef NEW_FIELD
169 #undef END_FIELD
170
171         fields[len] = NULL;
172         if (ret_len != NULL)
173                 *ret_len = len;
174         if (ret_fields != NULL)
175                 *ret_fields = fields;
176         else
177                 free (fields);
178         return (CMD_OK);
179 } /* int cmd_split */
180
181 /*
182  * public API
183  */
184
185 void cmd_error (cmd_status_t status, cmd_error_handler_t *err,
186                 const char *format, ...)
187 {
188         va_list ap;
189
190         if ((err == NULL) || (err->cb == NULL))
191                 return;
192
193         va_start (ap, format);
194         err->cb (err->ud, status, format, ap);
195         va_end (ap);
196 } /* void cmd_error */
197
198 cmd_status_t cmd_parsev (size_t argc, char **argv,
199                 cmd_t *ret_cmd, cmd_error_handler_t *err)
200 {
201         char *command = NULL;
202
203         if ((argc < 1) || (argv == NULL) || (ret_cmd == NULL))
204         {
205                 errno = EINVAL;
206                 cmd_error (CMD_ERROR, err, "Missing command.");
207                 return CMD_ERROR;
208         }
209
210         memset (ret_cmd, 0, sizeof (*ret_cmd));
211         command = argv[0];
212         if (strcasecmp ("FLUSH", command) == 0)
213         {
214                 ret_cmd->type = CMD_FLUSH;
215                 return cmd_parse_flush (argc - 1, argv + 1,
216                                 &ret_cmd->cmd.flush, err);
217         }
218         else if (strcasecmp ("GETVAL", command) == 0)
219         {
220                 ret_cmd->type = CMD_GETVAL;
221                 return cmd_parse_getval (argc - 1, argv + 1,
222                                 &ret_cmd->cmd.getval, err);
223         }
224         else if (strcasecmp ("LISTVAL", command) == 0)
225         {
226                 ret_cmd->type = CMD_LISTVAL;
227                 return cmd_parse_listval (argc - 1, argv + 1,
228                                 &ret_cmd->cmd.listval, err);
229         }
230         else if (strcasecmp ("PUTVAL", command) == 0)
231         {
232                 ret_cmd->type = CMD_PUTVAL;
233                 return cmd_parse_putval (argc - 1, argv + 1,
234                                 &ret_cmd->cmd.putval, err);
235         }
236         else
237         {
238                 ret_cmd->type = CMD_UNKNOWN;
239                 cmd_error (CMD_UNKNOWN_COMMAND, err,
240                                 "Unknown command `%s'.", command);
241                 return (CMD_UNKNOWN_COMMAND);
242         }
243
244         return (CMD_OK);
245 } /* cmd_status_t cmd_parsev */
246
247 cmd_status_t cmd_parse (char *buffer,
248                 cmd_t *ret_cmd, cmd_error_handler_t *err)
249 {
250         char **fields = NULL;
251         size_t fields_num = 0;
252         cmd_status_t status;
253
254         if ((status = cmd_split (buffer, &fields_num, &fields, err)) != CMD_OK)
255                 return status;
256
257         status = cmd_parsev (fields_num, fields, ret_cmd, err);
258         free (fields);
259         return (status);
260 } /* cmd_status_t cmd_parse */
261
262 void cmd_destroy (cmd_t *cmd)
263 {
264         if (cmd == NULL)
265                 return;
266
267         switch (cmd->type)
268         {
269                 case CMD_UNKNOWN:
270                         /* nothing to do */
271                         break;
272                 case CMD_FLUSH:
273                         cmd_destroy_flush (&cmd->cmd.flush);
274                         break;
275                 case CMD_GETVAL:
276                         cmd_destroy_getval (&cmd->cmd.getval);
277                         break;
278                 case CMD_LISTVAL:
279                         cmd_destroy_listval (&cmd->cmd.listval);
280                         break;
281                 case CMD_PUTVAL:
282                         cmd_destroy_putval (&cmd->cmd.putval);
283                         break;
284         }
285 } /* void cmd_destroy */
286
287 cmd_status_t cmd_parse_option (char *field,
288                 char **ret_key, char **ret_value, cmd_error_handler_t *err)
289 {
290         char *key, *value;
291
292         if (field == NULL)
293         {
294                 errno = EINVAL;
295                 cmd_error (CMD_ERROR, err, "Invalid argument to cmd_parse_option.");
296                 return (CMD_ERROR);
297         }
298         key = value = field;
299
300         /* Look for the equal sign. */
301         while (isalnum ((int)value[0]) || (value[0] == '_') || (value[0] == ':'))
302                 value++;
303         if ((value[0] != '=') || (value == key))
304         {
305                 /* Whether this is a fatal error is up to the caller. */
306                 return (CMD_NO_OPTION);
307         }
308         *value = '\0';
309         value++;
310
311         if (ret_key != NULL)
312                 *ret_key = key;
313         if (ret_value != NULL)
314                 *ret_value = value;
315
316         return (CMD_OK);
317 } /* cmd_status_t cmd_parse_option */
318
319 void cmd_error_fh (void *ud, cmd_status_t status,
320                 const char *format, va_list ap)
321 {
322         FILE *fh = ud;
323         int code = -1;
324         char buf[1024];
325
326         if (status == CMD_OK)
327                 code = 0;
328
329         vsnprintf (buf, sizeof(buf), format, ap);
330         buf[sizeof (buf) - 1] = '\0';
331         if (fprintf (fh, "%i %s\n", code, buf) < 0)
332         {
333                 char errbuf[1024];
334                 WARNING ("utils_cmds: failed to write to file-handle #%i: %s",
335                                 fileno (fh), sstrerror (errno, errbuf, sizeof (errbuf)));
336                 return;
337         }
338
339         fflush (fh);
340 } /* void cmd_error_fh */
341
342 /* vim: set sw=4 ts=4 tw=78 noexpandtab : */