17dbccc958b9f4c85aba4774e45a84745fd3c387
[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=0;
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     EXPECTED_OPEN_QUOTE,
113     EXPECTED_OPEN_BRACE,
114     EXPECTED_OPEN_PAREN,
115     EXPECTED_COMMA,
116     EXPECTED_START_RECORD_DATA,
117     EXPECTED_SEMICOLON,
118     EXPECTED_RECORD_DATA
119 };
120
121 static void parse_record_int(RECDB *recdb, char **pname, struct record_data **prd);
122
123 /* allocation functions */
124
125 #define alloc_record_data_int() malloc(sizeof(struct record_data))
126
127 struct record_data *
128 alloc_record_data_qstring(const char *string)
129 {
130     struct record_data *rd;
131     rd = alloc_record_data_int();
132     SET_RECORD_QSTRING(rd, string);
133     return rd;
134 }
135
136 struct record_data *
137 alloc_record_data_object(dict_t obj)
138 {
139     struct record_data *rd;
140     rd = alloc_record_data_int();
141     SET_RECORD_OBJECT(rd, obj);
142     return rd;
143 }
144
145 struct record_data *
146 alloc_record_data_string_list(struct string_list *slist)
147 {
148     struct record_data *rd;
149     rd = alloc_record_data_int();
150     SET_RECORD_STRING_LIST(rd, slist);
151     return rd;
152 }
153
154 struct string_list*
155 alloc_string_list(int size)
156 {
157     struct string_list *slist;
158     slist = malloc(sizeof(struct string_list));
159     slist->used = 0;
160     slist->size = size;
161     slist->list = slist->size ? malloc(size*sizeof(char*)) : NULL;
162     return slist;
163 }
164
165 dict_t
166 alloc_database(void)
167 {
168     dict_t db = dict_new();
169     dict_set_free_data(db, free_record_data);
170     return db;
171 }
172
173 /* misc. operations */
174
175 void
176 string_list_append(struct string_list *slist, char *string)
177 {
178     if (slist->used == slist->size) {
179         if (slist->size) {
180             slist->size <<= 1;
181             slist->list = realloc(slist->list, slist->size*sizeof(char*));
182         } else {
183             slist->size = 4;
184             slist->list = malloc(slist->size*sizeof(char*));
185         }
186     }
187     slist->list[slist->used++] = string;
188 }
189
190 struct string_list *
191 string_list_copy(struct string_list *slist)
192 {
193     struct string_list *new_list;
194     unsigned int i;
195     new_list = alloc_string_list(slist->size);
196     new_list->used = slist->used;
197     for (i=0; i<new_list->used; i++) {
198         new_list->list[i] = strdup(slist->list[i]);
199     }
200     return new_list;
201 }
202
203 int slist_compare_two(const void *pa, const void *pb)
204 {
205     return irccasecmp(*(const char**)pa, *(const char **)pb);
206 }
207
208 void
209 string_list_sort(struct string_list *slist)
210 {
211     qsort(slist->list, slist->used, sizeof(slist->list[0]), slist_compare_two);
212 }
213
214 struct record_data*
215 database_get_path(dict_t db, const char *path)
216 {
217     char *new_path = strdup(path), *orig_path = new_path;
218     char *part;
219     struct record_data *rd;
220
221     for (part=new_path; *new_path; new_path++) {
222         if (*new_path != '/') continue;
223         *new_path = 0;
224
225         rd = dict_find(db, part, NULL);
226         if (!rd || rd->type != RECDB_OBJECT) {
227             free(orig_path);
228             return NULL;
229         }
230
231         db = rd->d.object;
232         part = new_path+1;
233     }
234
235     rd = dict_find(db, part, NULL);
236     free(orig_path);
237     return rd;
238 }
239
240 void*
241 database_get_data(dict_t db, const char *path, enum recdb_type type)
242 {
243     struct record_data *rd = database_get_path(db, path);
244     return (rd && rd->type == type) ? rd->d.whatever : NULL;
245 }
246
247 /* free functions */
248
249 void
250 free_string_list(struct string_list *slist)
251 {
252     unsigned int i;
253     if (!slist)
254         return;
255     for (i=0; i<slist->used; i++)
256         free(slist->list[i]);
257     free(slist->list);
258     free(slist);
259 }
260
261 void
262 free_record_data(void *rdata)
263 {
264     struct record_data *r = rdata;
265     switch (r->type) {
266     case RECDB_INVALID: break;
267     case RECDB_QSTRING: free(r->d.qstring); break;
268     case RECDB_OBJECT: dict_delete(r->d.object); break;
269     case RECDB_STRING_LIST: free_string_list(r->d.slist); break;
270     }
271     free(r);
272 }
273
274 /* parse functions */
275
276 static int
277 dbeof(RECDB *recdb)
278 {
279     switch (recdb->type) {
280         case RECDB_FILE:
281             return feof(recdb->f);
282             break;
283         case RECDB_STRING:
284             return !*recdb->s;
285             break;
286         case RECDB_MMAP:
287             return ((size_t)recdb->pos >= recdb->length);
288             break;
289         default:
290             return 1;
291             break;
292     }
293 }
294
295 static int
296 dbgetc(RECDB *recdb)
297 {
298     int res;
299     switch (recdb->type) {
300         case RECDB_FILE:
301             res = fgetc(recdb->f);
302             break;
303         case RECDB_STRING:
304         case RECDB_MMAP:
305             res = dbeof(recdb) ? EOF : recdb->s[recdb->pos++];
306             break;
307         default:
308             res = EOF;
309             break;
310     }
311     if (res == EOL) recdb->ctx.line++, recdb->ctx.col=1;
312     else if (res != EOF) recdb->ctx.col++;
313     return res;
314 }
315
316 static void
317 dbungetc(int c, RECDB *recdb)
318 {
319     switch (recdb->type) {
320         case RECDB_FILE:
321             ungetc(c, recdb->f);
322             break;
323         case RECDB_STRING:
324         case RECDB_MMAP:
325             recdb->s[--recdb->pos] = c;
326             break;
327     }
328     if (c == EOL) recdb->ctx.line--, recdb->ctx.col=-1;
329     else recdb->ctx.col--;
330 }
331
332 /* returns first non-whitespace, non-comment character (-1 for EOF found) */
333 int
334 parse_skip_ws(RECDB *recdb)
335 {
336     int c, d, in_comment = 0;
337     while (!dbeof(recdb)) {
338         c = dbgetc(recdb);
339         if (c == EOF) return EOF;
340         if (isspace(c)) continue;
341         if (c != '/') return c;
342         if ((d = dbgetc(recdb)) == '*') {
343             /* C style comment, with slash star comment star slash */
344             in_comment = 1;
345             do {
346                 do {
347                     c = dbgetc(recdb);
348                 } while (c != '*' && c != EOF);
349                 if ((c = dbgetc(recdb)) == '/') in_comment = 0;
350             } while (in_comment);
351         } else if (d == '/') {
352             /* C++ style comment, with slash slash comment newline */
353             do {
354                 c = dbgetc(recdb);
355             } while (c != EOF && c != EOL);
356         } else {
357             if (d != EOF) dbungetc(d, recdb);
358             return c;
359         }
360     }
361     return -1;
362 }
363
364 char *
365 parse_qstring(RECDB *recdb)
366 {
367     char *buff;
368     int used=0, size=8, c;
369     struct recdb_context start_ctx;
370     unsigned int i;
371
372     if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
373     start_ctx = recdb->ctx;
374     if (c != '"') ABORT(recdb, EXPECTED_OPEN_QUOTE, c);
375     buff = malloc(size);
376     while (!dbeof(recdb) && (c = dbgetc(recdb)) != '"') {
377         if (c != '\\') {
378             /* There should never be a literal newline, as it is saved as a \n */
379             if (c == EOL) {
380                 dbungetc(c, recdb);
381                 ABORT(recdb, UNTERMINATED_STRING, ' ');
382             }
383             buff[used++] = c;
384         } else {
385             switch (c = dbgetc(recdb)) {
386                 case '0': /* \<octal>, 000 through 377 */
387                 case '1':
388                 case '2':
389                 case '3':
390                 case '4':
391                 case '5':
392                 case '6':
393                 case '7':
394                     {
395                         char digits[3] = { (char)c, '\0', '\0' };
396                         for (i=1; i < 3; i++) {
397                             /* Maximum of \377, so there's a max of 2 digits
398                              * if digits[0] > '3' (no \400, but \40 is fine) */
399                             if (i == 2 && digits[0] > '3') {
400                                 break;
401                             }
402                             if ((c = dbgetc(recdb)) == EOF) {
403                                 break;
404                             }
405                             if ((c < '0') || (c > '7')) {
406                                 dbungetc(c, recdb);
407                                 break;
408                             }
409                             digits[i] = (char)c;
410                         }
411                         if (i) {
412                             c = (int)strtol(digits, NULL, 8);
413                             buff[used++] = c;
414                         } else {
415                             buff[used++] = '\0';
416                         }
417                     }
418                     break;
419                 case 'x': /* Hex */
420                     {
421                         char digits[3] = { '\0', '\0', '\0' };
422                         for (i=0; i < 2; i++) {
423                             if ((c = dbgetc(recdb)) == EOF) {
424                                 break;
425                             }
426                             if (!isxdigit(c)) {
427                                 dbungetc(c, recdb);
428                                 break;
429                             }
430                             digits[i] = (char)c;
431                         }
432                         if (i) {
433                             c = (int)strtol(digits, NULL, 16);
434                             buff[used++] = c;
435                         } else {
436                             buff[used++] = '\\';
437                             buff[used++] = 'x';
438                         }
439                     }
440                     break;
441                 case 'a': buff[used++] = '\a'; break;
442                 case 'b': buff[used++] = '\b'; break;
443                 case 't': buff[used++] = '\t'; break;
444                 case 'n': buff[used++] = EOL; break;
445                 case 'v': buff[used++] = '\v'; break;
446                 case 'f': buff[used++] = '\f'; break;
447                 case 'r': buff[used++] = '\r'; break;
448                 case '\\': buff[used++] = '\\'; break;
449                 case '"': buff[used++] = '"'; break;
450                 default: buff[used++] = '\\'; buff[used++] = c; break;
451             }
452         }
453         if (used == size) {
454             size <<= 1;
455             buff = realloc(buff, size);
456         }
457     }
458     if (c != '"' && dbeof(recdb)) {
459         free(buff);
460         recdb->ctx = start_ctx;
461         ABORT(recdb, UNTERMINATED_STRING, EOF);
462     }
463     buff[used] = 0;
464     return buff;
465 }
466
467 dict_t
468 parse_object(RECDB *recdb)
469 {
470     dict_t obj;
471     char *name;
472     struct record_data *rd;
473     int c;
474     if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
475     if (c != '{') ABORT(recdb, EXPECTED_OPEN_BRACE, c);
476     obj = alloc_object();
477     dict_set_free_keys(obj, free);
478     while (!dbeof(recdb)) {
479         if ((c = parse_skip_ws(recdb)) == '}') break;
480         if (c == EOF) break;
481         dbungetc(c, recdb);
482         parse_record_int(recdb, &name, &rd);
483         dict_insert(obj, name, rd);
484     }
485     return obj;
486 }
487
488 struct string_list *
489 parse_string_list(RECDB *recdb)
490 {
491     struct string_list *slist;
492     int c;
493     if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
494     if (c != '(') ABORT(recdb, EXPECTED_OPEN_PAREN, c);
495     slist = alloc_string_list(4);
496     while (true) {
497         c = parse_skip_ws(recdb);
498         if (c == EOF || c == ')') break;
499         dbungetc(c, recdb);
500         string_list_append(slist, parse_qstring(recdb));
501         c = parse_skip_ws(recdb);
502         if (c == EOF || c == ')') break;
503         if (c != ',') ABORT(recdb, EXPECTED_COMMA, c);
504     }
505     return slist;
506 }
507
508 static void
509 parse_record_int(RECDB *recdb, char **pname, struct record_data **prd)
510 {
511     int c;
512     *pname = parse_qstring(recdb);
513     c = parse_skip_ws(recdb);
514     if (c == EOF) {
515         if (!*pname) return;
516         free(*pname);
517         ABORT(recdb, EXPECTED_RECORD_DATA, EOF);
518     }
519     if (c == '=') c = parse_skip_ws(recdb);
520     dbungetc(c, recdb);
521     *prd = malloc(sizeof(**prd));
522     switch (c) {
523     case '"':
524         /* Don't use SET_RECORD_QSTRING, since that does an extra strdup() of the string. */
525         (*prd)->type = RECDB_QSTRING;
526         (*prd)->d.qstring = parse_qstring(recdb);
527         break;
528     case '{': SET_RECORD_OBJECT(*prd, parse_object(recdb)); break;
529     case '(': SET_RECORD_STRING_LIST(*prd, parse_string_list(recdb)); break;
530     default: ABORT(recdb, EXPECTED_START_RECORD_DATA, c);
531     }
532     if ((c = parse_skip_ws(recdb)) != ';') ABORT(recdb, EXPECTED_SEMICOLON, c);
533 }
534
535 static dict_t
536 parse_database_int(RECDB *recdb)
537 {
538     char *name;
539     struct record_data *rd;
540     dict_t db = alloc_database();
541     dict_set_free_keys(db, free);
542     while (!dbeof(recdb)) {
543         parse_record_int(recdb, &name, &rd);
544         if (name) dict_insert(db, name, rd);
545     }
546     return db;
547 }
548
549 const char *
550 failure_reason(int code)
551 {
552     const char *reason;
553     switch (code >> 8) {
554     case UNTERMINATED_STRING: reason = "Unterminated string"; break;
555     case EXPECTED_OPEN_QUOTE: reason = "Expected '\"'"; break;
556     case EXPECTED_OPEN_BRACE: reason = "Expected '{'"; break;
557     case EXPECTED_OPEN_PAREN: reason = "Expected '('"; break;
558     case EXPECTED_COMMA: reason = "Expected ','"; break;
559     case EXPECTED_START_RECORD_DATA: reason = "Expected start of some record data"; break;
560     case EXPECTED_SEMICOLON: reason = "Expected ';'"; break;
561     case EXPECTED_RECORD_DATA: reason = "Expected record data"; break;
562     default: reason = "Unknown error";
563     }
564     if (code == -1) reason = "Premature end of file";
565     return reason;
566 }
567
568 void
569 explain_failure(RECDB *recdb, int code)
570 {
571     log_module(MAIN_LOG, LOG_ERROR, "%s (got '%c') at %s line %d column %d.",
572                failure_reason(code), code & 255,
573                recdb->source, recdb->ctx.line, recdb->ctx.col);
574 }
575
576 const char *
577 parse_record(const char *text, char **pname, struct record_data **prd)
578 {
579     RECDB recdb;
580     int res;
581     *pname = NULL;
582     *prd = NULL;
583     recdb.source = "<user-supplied text>";
584     recdb.f = NULL;
585     recdb.s = strdup(text);
586     recdb.length = strlen(text);
587     recdb.pos = 0;
588     recdb.type = RECDB_STRING;
589     recdb.ctx.line = recdb.ctx.col = 1;
590     if ((res = setjmp(recdb.env)) == 0) {
591         parse_record_int(&recdb, pname, prd);
592         return 0;
593     } else {
594         free(*pname);
595         free(*prd);
596         return failure_reason(res);
597     }
598 }
599
600 dict_t
601 parse_database(const char *filename)
602 {
603     RECDB recdb;
604     int res;
605     dict_t db;
606     struct stat statinfo;
607
608     recdb.source = filename;
609     if (!(recdb.f = fopen(filename, "r"))) {
610         log_module(MAIN_LOG, LOG_ERROR, "Unable to open database file '%s' for reading: %s", filename, strerror(errno));
611         return NULL;
612     }
613
614     if (fstat(fileno(recdb.f), &statinfo)) {
615         log_module(MAIN_LOG, LOG_ERROR, "Unable to fstat database file '%s': %s", filename, strerror(errno));
616         return NULL;
617     }
618     recdb.length = (size_t)statinfo.st_size;
619
620 #ifdef HAVE_MMAP
621     /* Try mmap */
622     if (!mmap_error && (recdb.s = mmap(NULL, recdb.length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fileno(recdb.f), 0)) != MAP_FAILED) {
623         recdb.type = RECDB_MMAP;
624         madvise(recdb.s, recdb.length, MADV_SEQUENTIAL);
625     } else {
626         /* Fall back to stdio */
627         if (!mmap_error) {
628             log_module(MAIN_LOG, LOG_WARNING, "Unable to mmap database file '%s' (falling back to stdio): %s", filename, strerror(errno));
629             mmap_error = 1;
630         }
631 #else
632     if (1) {
633 #endif
634         recdb.s = NULL;
635         recdb.type = RECDB_FILE;
636     }
637
638     recdb.ctx.line = recdb.ctx.col = 1;
639     recdb.pos = 0;
640
641     if ((res = setjmp(recdb.env)) == 0) {
642         db = parse_database_int(&recdb);
643     } else {
644         explain_failure(&recdb, res);
645         _exit(1);
646     }
647
648     switch (recdb.type) {
649         case RECDB_MMAP:
650 #ifdef HAVE_MMAP
651             munmap(recdb.s, recdb.length);
652 #endif
653         case RECDB_FILE:
654             fclose(recdb.f);
655             break;
656         /* Appease gcc */
657         default:
658             break;
659     }
660     return db;
661 }