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