fix possible crash on user deletion
[srvx.git] / src / recdb.c
1 /* recdb.c - recursive/record database implementation
2  * Copyright 2000-2004 srvx Development Team
3  *
4  * This file is part of srvx.
5  *
6  * srvx is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with srvx; if not, write to the Free Software Foundation,
18  * Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
19  */
20
21 #include "recdb.h"
22 #include "log.h"
23
24 #ifdef HAVE_FCNTL_H
25 #include <fcntl.h>
26 #endif
27 #ifdef HAVE_SYS_MMAN_H
28 #include <sys/mman.h>
29 #endif
30 #ifdef HAVE_SYS_STAT_H
31 #include <sys/stat.h>
32 #endif
33
34 /* 4 MiB on x86 */
35 #define MMAP_MAP_LENGTH (getpagesize()*1024)
36
37 /* file format (grammar in Backus-Naur Form):
38  *
39  * database := record*
40  * record := qstring [ '=' ] ( qstring | object | stringlist ) ';'
41  * qstring := '"' ( [^\\] | ('\\' [\\n]) )* '"'
42  * object := '{' record* '}'
43  * stringlist := '(' [ qstring [',' qstring]* ] ')'
44  *
45  */
46
47 /* when a database or object is read from disk, it is represented as
48  * a dictionary object, keys are names (what's left of the '=') and
49  * values are 'struct record_data's
50  */
51
52 struct recdb_context {
53     int line;
54     int col;
55 };
56
57 enum recdb_filetype {
58     RECDB_FILE,
59     RECDB_STRING,
60     RECDB_MMAP
61 };
62
63 typedef struct recdb_file {
64     const char *source;
65     FILE *f; /* For RECDB_FILE, RECDB_MMAP */
66     char *s; /* For RECDB_STRING, RECDB_MMAP */
67     enum recdb_filetype type;
68     size_t length;
69     off_t pos;
70     struct recdb_context ctx;
71     jmp_buf env;
72 } RECDB;
73
74 typedef struct recdb_outfile {
75     FILE *f; /* For RECDB_FILE, RECDB_MMAP */
76     char *s; /* For RECDB_STRING, RECDB_MMAP */
77     union {
78         struct { /* For RECDB_STRING */
79             size_t chunksize;
80             size_t alloc_length;
81         } s;
82         struct { /* For RECDB_MMAP */
83             off_t mmap_begin;
84             size_t mmap_length;
85         } m;
86     } state;
87     enum recdb_filetype type;
88     off_t pos;
89     int tablvl;
90 #ifdef NDEBUG
91     int need_tab;
92 #endif
93 } RECDB_OUT;
94
95 #ifdef HAVE_MMAP
96 static int mmap_error;
97 #endif
98
99 #define EOL '\n'
100
101 #if 1
102 #define ABORT(recdb, code, ch) longjmp((recdb)->env, ((code) << 8) | (ch))
103 #else
104 static void
105 ABORT(RECDB *recdb, int code, unsigned char ch) {
106     longjmp(recdb->env, code << 8 | ch);
107 }
108 #endif
109
110 enum fail_codes {
111     UNTERMINATED_STRING,
112     UNTERMINATED_COMMENT,
113     EXPECTED_OPEN_QUOTE,
114     EXPECTED_OPEN_BRACE,
115     EXPECTED_OPEN_PAREN,
116     EXPECTED_COMMA,
117     EXPECTED_START_RECORD_DATA,
118     EXPECTED_SEMICOLON,
119     EXPECTED_RECORD_DATA
120 };
121
122 static void parse_record_int(RECDB *recdb, char **pname, struct record_data **prd);
123
124 /* allocation functions */
125
126 #define alloc_record_data_int() malloc(sizeof(struct record_data))
127
128 struct record_data *
129 alloc_record_data_qstring(const char *string)
130 {
131     struct record_data *rd;
132     rd = alloc_record_data_int();
133     SET_RECORD_QSTRING(rd, string);
134     return rd;
135 }
136
137 struct record_data *
138 alloc_record_data_object(dict_t obj)
139 {
140     struct record_data *rd;
141     rd = alloc_record_data_int();
142     SET_RECORD_OBJECT(rd, obj);
143     return rd;
144 }
145
146 struct record_data *
147 alloc_record_data_string_list(struct string_list *slist)
148 {
149     struct record_data *rd;
150     rd = alloc_record_data_int();
151     SET_RECORD_STRING_LIST(rd, slist);
152     return rd;
153 }
154
155 struct string_list*
156 alloc_string_list(int size)
157 {
158     struct string_list *slist;
159     slist = malloc(sizeof(struct string_list));
160     slist->used = 0;
161     slist->size = size;
162     slist->list = slist->size ? malloc(size*sizeof(char*)) : NULL;
163     return slist;
164 }
165
166 dict_t
167 alloc_database(void)
168 {
169     dict_t db = dict_new();
170     dict_set_free_data(db, free_record_data);
171     return db;
172 }
173
174 /* misc. operations */
175
176 void
177 string_list_append(struct string_list *slist, char *string)
178 {
179     if (slist->used == slist->size) {
180         if (slist->size) {
181             slist->size <<= 1;
182             slist->list = realloc(slist->list, slist->size*sizeof(char*));
183         } else {
184             slist->size = 4;
185             slist->list = malloc(slist->size*sizeof(char*));
186         }
187     }
188     slist->list[slist->used++] = string;
189 }
190
191 struct string_list *
192 string_list_copy(struct string_list *slist)
193 {
194     struct string_list *new_list;
195     unsigned int i;
196     new_list = alloc_string_list(slist->size);
197     new_list->used = slist->used;
198     for (i=0; i<new_list->used; i++) {
199         new_list->list[i] = strdup(slist->list[i]);
200     }
201     return new_list;
202 }
203
204 int slist_compare_two(const void *pa, const void *pb)
205 {
206     return irccasecmp(*(const char**)pa, *(const char **)pb);
207 }
208
209 void
210 string_list_sort(struct string_list *slist)
211 {
212     qsort(slist->list, slist->used, sizeof(slist->list[0]), slist_compare_two);
213 }
214
215 struct record_data*
216 database_get_path(dict_t db, const char *path)
217 {
218     char *new_path = strdup(path), *orig_path = new_path;
219     char *part;
220     struct record_data *rd;
221
222     for (part=new_path; *new_path; new_path++) {
223         if (*new_path != '/') continue;
224         *new_path = 0;
225
226         rd = dict_find(db, part, NULL);
227         if (!rd || rd->type != RECDB_OBJECT) {
228             free(orig_path);
229             return NULL;
230         }
231
232         db = rd->d.object;
233         part = new_path+1;
234     }
235
236     rd = dict_find(db, part, NULL);
237     free(orig_path);
238     return rd;
239 }
240
241 void*
242 database_get_data(dict_t db, const char *path, enum recdb_type type)
243 {
244     struct record_data *rd = database_get_path(db, path);
245     return (rd && rd->type == type) ? rd->d.whatever : NULL;
246 }
247
248 /* free functions */
249
250 void
251 free_string_list(struct string_list *slist)
252 {
253     unsigned int i;
254     if (!slist)
255         return;
256     for (i=0; i<slist->used; i++)
257         free(slist->list[i]);
258     free(slist->list);
259     free(slist);
260 }
261
262 void
263 free_record_data(void *rdata)
264 {
265     struct record_data *r = rdata;
266     switch (r->type) {
267     case RECDB_INVALID: break;
268     case RECDB_QSTRING: free(r->d.qstring); break;
269     case RECDB_OBJECT: dict_delete(r->d.object); break;
270     case RECDB_STRING_LIST: free_string_list(r->d.slist); break;
271     }
272     free(r);
273 }
274
275 /* parse functions */
276
277 static int
278 dbeof(RECDB *recdb)
279 {
280     switch (recdb->type) {
281         case RECDB_FILE:
282             return feof(recdb->f);
283             break;
284         case RECDB_STRING:
285             return !*recdb->s;
286             break;
287         case RECDB_MMAP:
288             return ((size_t)recdb->pos >= recdb->length);
289             break;
290         default:
291             return 1;
292             break;
293     }
294 }
295
296 static int
297 dbgetc(RECDB *recdb)
298 {
299     int res;
300     switch (recdb->type) {
301         case RECDB_FILE:
302             res = fgetc(recdb->f);
303             break;
304         case RECDB_STRING:
305         case RECDB_MMAP:
306             res = dbeof(recdb) ? EOF : recdb->s[recdb->pos++];
307             break;
308         default:
309             res = EOF;
310             break;
311     }
312     if (res == EOL) recdb->ctx.line++, recdb->ctx.col=1;
313     else if (res != EOF) recdb->ctx.col++;
314     return res;
315 }
316
317 static void
318 dbungetc(int c, RECDB *recdb)
319 {
320     switch (recdb->type) {
321         case RECDB_FILE:
322             ungetc(c, recdb->f);
323             break;
324         case RECDB_STRING:
325         case RECDB_MMAP:
326             recdb->s[--recdb->pos] = c;
327             break;
328     }
329     if (c == EOL) recdb->ctx.line--, recdb->ctx.col=-1;
330     else recdb->ctx.col--;
331 }
332
333 /* returns first non-whitespace, non-comment character (-1 for EOF found) */
334 int
335 parse_skip_ws(RECDB *recdb)
336 {
337     int c, d, in_comment = 0;
338     while (!dbeof(recdb)) {
339         c = dbgetc(recdb);
340         if (c == EOF) return EOF;
341         if (isspace(c)) continue;
342         if (c != '/') return c;
343         if ((d = dbgetc(recdb)) == '*') {
344             /* C style comment, with slash star comment star slash */
345             in_comment = 1;
346             do {
347                 do {
348                     c = dbgetc(recdb);
349                     if (c == EOF) ABORT(recdb, UNTERMINATED_COMMENT, c);
350                 } while (c != '*');
351                 if ((c = dbgetc(recdb)) == '/') in_comment = 0;
352                 if (c == EOF) ABORT(recdb, UNTERMINATED_COMMENT, c);
353             } while (in_comment);
354         } else if (d == '/') {
355             /* C++ style comment, with slash slash comment newline */
356             do {
357                 c = dbgetc(recdb);
358             } while (c != EOF && c != EOL);
359         } else {
360             if (d != EOF) dbungetc(d, recdb);
361             return c;
362         }
363     }
364     return -1;
365 }
366
367 char *
368 parse_qstring(RECDB *recdb)
369 {
370     char *buff;
371     int used=0, size=8, c;
372     struct recdb_context start_ctx;
373     unsigned int i;
374
375     if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
376     start_ctx = recdb->ctx;
377     if (c != '"') ABORT(recdb, EXPECTED_OPEN_QUOTE, c);
378     buff = malloc(size);
379     while (!dbeof(recdb) && (c = dbgetc(recdb)) != '"') {
380         if (c != '\\') {
381             /* There should never be a literal newline, as it is saved as a \n */
382             if (c == EOL) {
383                 dbungetc(c, recdb);
384                 ABORT(recdb, UNTERMINATED_STRING, ' ');
385             }
386             buff[used++] = c;
387         } else {
388             switch (c = dbgetc(recdb)) {
389                 case '0': /* \<octal>, 000 through 377 */
390                 case '1':
391                 case '2':
392                 case '3':
393                 case '4':
394                 case '5':
395                 case '6':
396                 case '7':
397                     {
398                         char digits[3] = { (char)c, '\0', '\0' };
399                         for (i=1; i < 3; i++) {
400                             /* Maximum of \377, so there's a max of 2 digits
401                              * if digits[0] > '3' (no \400, but \40 is fine) */
402                             if (i == 2 && digits[0] > '3') {
403                                 break;
404                             }
405                             if ((c = dbgetc(recdb)) == EOF) {
406                                 break;
407                             }
408                             if ((c < '0') || (c > '7')) {
409                                 dbungetc(c, recdb);
410                                 break;
411                             }
412                             digits[i] = (char)c;
413                         }
414                         if (i) {
415                             c = (int)strtol(digits, NULL, 8);
416                             buff[used++] = c;
417                         } else {
418                             buff[used++] = '\0';
419                         }
420                     }
421                     break;
422                 case 'x': /* Hex */
423                     {
424                         char digits[3] = { '\0', '\0', '\0' };
425                         for (i=0; i < 2; i++) {
426                             if ((c = dbgetc(recdb)) == EOF) {
427                                 break;
428                             }
429                             if (!isxdigit(c)) {
430                                 dbungetc(c, recdb);
431                                 break;
432                             }
433                             digits[i] = (char)c;
434                         }
435                         if (i) {
436                             c = (int)strtol(digits, NULL, 16);
437                             buff[used++] = c;
438                         } else {
439                             buff[used++] = '\\';
440                             buff[used++] = 'x';
441                         }
442                     }
443                     break;
444                 case 'a': buff[used++] = '\a'; break;
445                 case 'b': buff[used++] = '\b'; break;
446                 case 't': buff[used++] = '\t'; break;
447                 case 'n': buff[used++] = EOL; break;
448                 case 'v': buff[used++] = '\v'; break;
449                 case 'f': buff[used++] = '\f'; break;
450                 case 'r': buff[used++] = '\r'; break;
451                 case '\\': buff[used++] = '\\'; break;
452                 case '"': buff[used++] = '"'; break;
453                 default: buff[used++] = '\\'; buff[used++] = c; break;
454             }
455         }
456         if (used == size) {
457             size <<= 1;
458             buff = realloc(buff, size);
459         }
460     }
461     if (c != '"' && dbeof(recdb)) {
462         free(buff);
463         recdb->ctx = start_ctx;
464         ABORT(recdb, UNTERMINATED_STRING, EOF);
465     }
466     buff[used] = 0;
467     return buff;
468 }
469
470 dict_t
471 parse_object(RECDB *recdb)
472 {
473     dict_t obj;
474     char *name;
475     struct record_data *rd;
476     int c;
477     if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
478     if (c != '{') ABORT(recdb, EXPECTED_OPEN_BRACE, c);
479     obj = alloc_object();
480     dict_set_free_keys(obj, free);
481     while (!dbeof(recdb)) {
482         if ((c = parse_skip_ws(recdb)) == '}') break;
483         if (c == EOF) break;
484         dbungetc(c, recdb);
485         parse_record_int(recdb, &name, &rd);
486         dict_insert(obj, name, rd);
487     }
488     return obj;
489 }
490
491 struct string_list *
492 parse_string_list(RECDB *recdb)
493 {
494     struct string_list *slist;
495     int c;
496     if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
497     if (c != '(') ABORT(recdb, EXPECTED_OPEN_PAREN, c);
498     slist = alloc_string_list(4);
499     while (true) {
500         c = parse_skip_ws(recdb);
501         if (c == EOF || c == ')') break;
502         dbungetc(c, recdb);
503         string_list_append(slist, parse_qstring(recdb));
504         c = parse_skip_ws(recdb);
505         if (c == EOF || c == ')') break;
506         if (c != ',') ABORT(recdb, EXPECTED_COMMA, c);
507     }
508     return slist;
509 }
510
511 static void
512 parse_record_int(RECDB *recdb, char **pname, struct record_data **prd)
513 {
514     int c;
515     *pname = parse_qstring(recdb);
516     c = parse_skip_ws(recdb);
517     if (c == EOF) {
518         if (!*pname) return;
519         free(*pname);
520         ABORT(recdb, EXPECTED_RECORD_DATA, EOF);
521     }
522     if (c == '=') c = parse_skip_ws(recdb);
523     dbungetc(c, recdb);
524     *prd = malloc(sizeof(**prd));
525     switch (c) {
526     case '"':
527         /* Don't use SET_RECORD_QSTRING, since that does an extra strdup() of the string. */
528         (*prd)->type = RECDB_QSTRING;
529         (*prd)->d.qstring = parse_qstring(recdb);
530         break;
531     case '{': SET_RECORD_OBJECT(*prd, parse_object(recdb)); break;
532     case '(': SET_RECORD_STRING_LIST(*prd, parse_string_list(recdb)); break;
533     default: ABORT(recdb, EXPECTED_START_RECORD_DATA, c);
534     }
535     if ((c = parse_skip_ws(recdb)) != ';') ABORT(recdb, EXPECTED_SEMICOLON, c);
536 }
537
538 static dict_t
539 parse_database_int(RECDB *recdb)
540 {
541     char *name;
542     struct record_data *rd;
543     dict_t db = alloc_database();
544     dict_set_free_keys(db, free);
545     while (!dbeof(recdb)) {
546         parse_record_int(recdb, &name, &rd);
547         if (name) dict_insert(db, name, rd);
548     }
549     return db;
550 }
551
552 const char *
553 failure_reason(int code)
554 {
555     const char *reason;
556     switch (code >> 8) {
557     case UNTERMINATED_STRING: reason = "Unterminated string"; break;
558     case UNTERMINATED_COMMENT: reason = "Unterminated comment"; break;
559     case EXPECTED_OPEN_QUOTE: reason = "Expected '\"'"; break;
560     case EXPECTED_OPEN_BRACE: reason = "Expected '{'"; break;
561     case EXPECTED_OPEN_PAREN: reason = "Expected '('"; break;
562     case EXPECTED_COMMA: reason = "Expected ','"; break;
563     case EXPECTED_START_RECORD_DATA: reason = "Expected start of some record data"; break;
564     case EXPECTED_SEMICOLON: reason = "Expected ';'"; break;
565     case EXPECTED_RECORD_DATA: reason = "Expected record data"; break;
566     default: reason = "Unknown error";
567     }
568     if (code == -1) reason = "Premature end of file";
569     return reason;
570 }
571
572 void
573 explain_failure(RECDB *recdb, int code)
574 {
575     static char msg[1024];
576     snprintf(msg, sizeof(msg), "%s (got '%c') at %s line %d column %d.",
577              failure_reason(code), code & 255,
578              recdb->source, recdb->ctx.line, recdb->ctx.col);
579     if (MAIN_LOG == NULL) {
580         fputs(msg, stderr);
581         fputc('\n', stderr);
582         fflush(stderr);
583     } else
584         log_module(MAIN_LOG, LOG_ERROR, "%s", msg);
585 }
586
587 const char *
588 parse_record(const char *text, char **pname, struct record_data **prd)
589 {
590     RECDB recdb;
591     int res;
592     *pname = NULL;
593     *prd = NULL;
594     recdb.source = "<user-supplied text>";
595     recdb.f = NULL;
596     recdb.s = strdup(text);
597     recdb.length = strlen(text);
598     recdb.pos = 0;
599     recdb.type = RECDB_STRING;
600     recdb.ctx.line = recdb.ctx.col = 1;
601     if ((res = setjmp(recdb.env)) == 0) {
602         parse_record_int(&recdb, pname, prd);
603         return 0;
604     } else {
605         free(*pname);
606         free(*prd);
607         return failure_reason(res);
608     }
609 }
610
611 dict_t
612 parse_database(const char *filename)
613 {
614     RECDB recdb;
615     int res;
616     dict_t db;
617     struct stat statinfo;
618
619     recdb.source = filename;
620     if (!(recdb.f = fopen(filename, "r"))) {
621         log_module(MAIN_LOG, LOG_ERROR, "Unable to open database file '%s' for reading: %s", filename, strerror(errno));
622         return NULL;
623     }
624
625     if (fstat(fileno(recdb.f), &statinfo)) {
626         log_module(MAIN_LOG, LOG_ERROR, "Unable to fstat database file '%s': %s", filename, strerror(errno));
627         fclose(recdb.f);
628         return NULL;
629     }
630     recdb.length = (size_t)statinfo.st_size;
631     if (recdb.length == 0) {
632         fclose(recdb.f);
633         return alloc_database();
634     }
635
636 #ifdef HAVE_MMAP
637     /* Try mmap */
638     if (!mmap_error && (recdb.s = mmap(NULL, recdb.length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fileno(recdb.f), 0)) != MAP_FAILED) {
639         recdb.type = RECDB_MMAP;
640         madvise(recdb.s, recdb.length, MADV_SEQUENTIAL);
641     } else {
642         /* Fall back to stdio */
643         if (!mmap_error) {
644             log_module(MAIN_LOG, LOG_WARNING, "Unable to mmap database file '%s' (falling back to stdio): %s", filename, strerror(errno));
645             mmap_error = 1;
646         }
647 #else
648     if (1) {
649 #endif
650         recdb.s = NULL;
651         recdb.type = RECDB_FILE;
652     }
653
654     recdb.ctx.line = recdb.ctx.col = 1;
655     recdb.pos = 0;
656
657     if ((res = setjmp(recdb.env)) == 0) {
658         db = parse_database_int(&recdb);
659     } else {
660         explain_failure(&recdb, res);
661         _exit(1);
662     }
663
664     switch (recdb.type) {
665         case RECDB_MMAP:
666 #ifdef HAVE_MMAP
667             munmap(recdb.s, recdb.length);
668 #endif
669         case RECDB_FILE:
670             fclose(recdb.f);
671             break;
672         /* Appease gcc */
673         default:
674             break;
675     }
676     return db;
677 }