]> jfr.im git - irc/evilnet/x3.git/blob - src/recdb.c
Minor typo in previous commit where returning 0 when it should have been 1 from opser...
[irc/evilnet/x3.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 x3.
5 *
6 * x3 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 3 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 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 assert(path != NULL);
244 if(!path)
245 log_module(MAIN_LOG, LOG_WARNING, "Null path in database_get_data()");
246 /* log_module(MAIN_LOG, LOG_DEBUG, "Reading config option '%s'", path); */
247 struct record_data *rd = database_get_path(db, path);
248 return (rd && rd->type == type) ? rd->d.whatever : NULL;
249 }
250
251 /* free functions */
252
253 void
254 free_string_list(struct string_list *slist)
255 {
256 unsigned int i;
257 if (!slist)
258 return;
259 for (i=0; i<slist->used; i++)
260 free(slist->list[i]);
261 free(slist->list);
262 free(slist);
263 }
264
265 void
266 free_record_data(void *rdata)
267 {
268 struct record_data *r = rdata;
269 switch (r->type) {
270 case RECDB_INVALID: break;
271 case RECDB_QSTRING: free(r->d.qstring); break;
272 case RECDB_OBJECT: dict_delete(r->d.object); break;
273 case RECDB_STRING_LIST: free_string_list(r->d.slist); break;
274 }
275 free(r);
276 }
277
278 /* parse functions */
279
280 static int
281 dbeof(RECDB *recdb)
282 {
283 switch (recdb->type) {
284 case RECDB_FILE:
285 return feof(recdb->f);
286 break;
287 case RECDB_STRING:
288 return !*recdb->s;
289 break;
290 case RECDB_MMAP:
291 return ((size_t)recdb->pos >= recdb->length);
292 break;
293 default:
294 return 1;
295 break;
296 }
297 }
298
299 static int
300 dbgetc(RECDB *recdb)
301 {
302 int res;
303 switch (recdb->type) {
304 case RECDB_FILE:
305 res = fgetc(recdb->f);
306 break;
307 case RECDB_STRING:
308 case RECDB_MMAP:
309 res = dbeof(recdb) ? EOF : recdb->s[recdb->pos++];
310 break;
311 default:
312 res = EOF;
313 break;
314 }
315 if (res == EOL) recdb->ctx.line++, recdb->ctx.col=1;
316 else if (res != EOF) recdb->ctx.col++;
317 return res;
318 }
319
320 static void
321 dbungetc(int c, RECDB *recdb)
322 {
323 switch (recdb->type) {
324 case RECDB_FILE:
325 ungetc(c, recdb->f);
326 break;
327 case RECDB_STRING:
328 case RECDB_MMAP:
329 recdb->s[--recdb->pos] = c;
330 break;
331 }
332 if (c == EOL) recdb->ctx.line--, recdb->ctx.col=-1;
333 else recdb->ctx.col--;
334 }
335
336 /* returns first non-whitespace, non-comment character (-1 for EOF found) */
337 int
338 parse_skip_ws(RECDB *recdb)
339 {
340 int c, d, in_comment = 0;
341 while (!dbeof(recdb)) {
342 c = dbgetc(recdb);
343 if (c == EOF) return EOF;
344 if (isspace(c)) continue;
345 if (c != '/') return c;
346 if ((d = dbgetc(recdb)) == '*') {
347 /* C style comment, with slash star comment star slash */
348 in_comment = 1;
349 do {
350 do {
351 c = dbgetc(recdb);
352 } while (c != '*' && c != EOF);
353 if ((c = dbgetc(recdb)) == '/') in_comment = 0;
354 } while (in_comment);
355 } else if (d == '/') {
356 /* C++ style comment, with slash slash comment newline */
357 do {
358 c = dbgetc(recdb);
359 } while (c != EOF && c != EOL);
360 } else {
361 if (d != EOF) dbungetc(d, recdb);
362 return c;
363 }
364 }
365 return -1;
366 }
367
368 char *
369 parse_qstring(RECDB *recdb)
370 {
371 char *buff;
372 int used=0, size=8, c;
373 struct recdb_context start_ctx;
374 unsigned int i;
375
376 if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
377 start_ctx = recdb->ctx;
378 if (c != '"') ABORT(recdb, EXPECTED_OPEN_QUOTE, c);
379 buff = malloc(size);
380 while (!dbeof(recdb) && (c = dbgetc(recdb)) != '"') {
381 if (c != '\\') {
382 /* There should never be a literal newline, as it is saved as a \n */
383 if (c == EOL) {
384 dbungetc(c, recdb);
385 ABORT(recdb, UNTERMINATED_STRING, ' ');
386 }
387 buff[used++] = c;
388 } else {
389 switch (c = dbgetc(recdb)) {
390 case '0': /* \<octal>, 000 through 377 */
391 case '1':
392 case '2':
393 case '3':
394 case '4':
395 case '5':
396 case '6':
397 case '7':
398 {
399 char digits[3] = { (char)c, '\0', '\0' };
400 for (i=1; i < 3; i++) {
401 /* Maximum of \377, so there's a max of 2 digits
402 * if digits[0] > '3' (no \400, but \40 is fine) */
403 if (i == 2 && digits[0] > '3') {
404 break;
405 }
406 if ((c = dbgetc(recdb)) == EOF) {
407 break;
408 }
409 if ((c < '0') || (c > '7')) {
410 dbungetc(c, recdb);
411 break;
412 }
413 digits[i] = (char)c;
414 }
415 if (i) {
416 c = (int)strtol(digits, NULL, 8);
417 buff[used++] = c;
418 } else {
419 buff[used++] = '\0';
420 }
421 }
422 break;
423 case 'x': /* Hex */
424 {
425 char digits[3] = { '\0', '\0', '\0' };
426 for (i=0; i < 2; i++) {
427 if ((c = dbgetc(recdb)) == EOF) {
428 break;
429 }
430 if (!isxdigit(c)) {
431 dbungetc(c, recdb);
432 break;
433 }
434 digits[i] = (char)c;
435 }
436 if (i) {
437 c = (int)strtol(digits, NULL, 16);
438 buff[used++] = c;
439 } else {
440 buff[used++] = '\\';
441 buff[used++] = 'x';
442 }
443 }
444 break;
445 case 'a': buff[used++] = '\a'; break;
446 case 'b': buff[used++] = '\b'; break;
447 case 't': buff[used++] = '\t'; break;
448 case 'n': buff[used++] = EOL; break;
449 case 'v': buff[used++] = '\v'; break;
450 case 'f': buff[used++] = '\f'; break;
451 case 'r': buff[used++] = '\r'; break;
452 case '\\': buff[used++] = '\\'; break;
453 case '"': buff[used++] = '"'; break;
454 default: buff[used++] = '\\'; buff[used++] = c; break;
455 }
456 }
457 if (used == size) {
458 size <<= 1;
459 buff = realloc(buff, size);
460 }
461 }
462 if (c != '"' && dbeof(recdb)) {
463 free(buff);
464 recdb->ctx = start_ctx;
465 ABORT(recdb, UNTERMINATED_STRING, EOF);
466 }
467 buff[used] = 0;
468 return buff;
469 }
470
471 dict_t
472 parse_object(RECDB *recdb)
473 {
474 dict_t obj;
475 char *name;
476 struct record_data *rd;
477 int c;
478 if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
479 if (c != '{') ABORT(recdb, EXPECTED_OPEN_BRACE, c);
480 obj = alloc_object();
481 dict_set_free_keys(obj, free);
482 while (!dbeof(recdb)) {
483 if ((c = parse_skip_ws(recdb)) == '}') break;
484 if (c == EOF) break;
485 dbungetc(c, recdb);
486 parse_record_int(recdb, &name, &rd);
487 dict_insert(obj, name, rd);
488 }
489 return obj;
490 }
491
492 struct string_list *
493 parse_string_list(RECDB *recdb)
494 {
495 struct string_list *slist;
496 int c;
497 if ((c = parse_skip_ws(recdb)) == EOF) return NULL;
498 if (c != '(') ABORT(recdb, EXPECTED_OPEN_PAREN, c);
499 slist = alloc_string_list(4);
500 while (true) {
501 c = parse_skip_ws(recdb);
502 if (c == EOF || c == ')') break;
503 dbungetc(c, recdb);
504 string_list_append(slist, parse_qstring(recdb));
505 c = parse_skip_ws(recdb);
506 if (c == EOF || c == ')') break;
507 if (c != ',') ABORT(recdb, EXPECTED_COMMA, c);
508 }
509 return slist;
510 }
511
512 static void
513 parse_record_int(RECDB *recdb, char **pname, struct record_data **prd)
514 {
515 int c;
516 *pname = parse_qstring(recdb);
517 c = parse_skip_ws(recdb);
518 if (c == EOF) {
519 if (!*pname) return;
520 free(*pname);
521 ABORT(recdb, EXPECTED_RECORD_DATA, EOF);
522 }
523 if (c == '=') c = parse_skip_ws(recdb);
524 dbungetc(c, recdb);
525 *prd = malloc(sizeof(**prd));
526 switch (c) {
527 case '"':
528 /* Don't use SET_RECORD_QSTRING, since that does an extra strdup() of the string. */
529 (*prd)->type = RECDB_QSTRING;
530 (*prd)->d.qstring = parse_qstring(recdb);
531 break;
532 case '{': SET_RECORD_OBJECT(*prd, parse_object(recdb)); break;
533 case '(': SET_RECORD_STRING_LIST(*prd, parse_string_list(recdb)); break;
534 default: ABORT(recdb, EXPECTED_START_RECORD_DATA, c);
535 }
536 if ((c = parse_skip_ws(recdb)) != ';') ABORT(recdb, EXPECTED_SEMICOLON, c);
537 }
538
539 static dict_t
540 parse_database_int(RECDB *recdb)
541 {
542 char *name;
543 struct record_data *rd;
544 dict_t db = alloc_database();
545 dict_set_free_keys(db, free);
546 while (!dbeof(recdb)) {
547 parse_record_int(recdb, &name, &rd);
548 if (name) dict_insert(db, name, rd);
549 }
550 return db;
551 }
552
553 const char *
554 failure_reason(int code)
555 {
556 const char *reason;
557 switch (code >> 8) {
558 case UNTERMINATED_STRING: reason = "Unterminated string"; 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 return alloc_database();
633 }
634
635 #ifdef HAVE_MMAP
636 /* Try mmap */
637 if (!mmap_error && (recdb.s = mmap(NULL, recdb.length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fileno(recdb.f), 0)) != MAP_FAILED) {
638 recdb.type = RECDB_MMAP;
639 madvise(recdb.s, recdb.length, MADV_SEQUENTIAL);
640 } else {
641 /* Fall back to stdio */
642 if (!mmap_error) {
643 log_module(MAIN_LOG, LOG_WARNING, "Unable to mmap database file '%s' (falling back to stdio): %s", filename, strerror(errno));
644 mmap_error = 1;
645 }
646 #else
647 if (1) {
648 #endif
649 recdb.s = NULL;
650 recdb.type = RECDB_FILE;
651 }
652
653 recdb.ctx.line = recdb.ctx.col = 1;
654 recdb.pos = 0;
655
656 if ((res = setjmp(recdb.env)) == 0) {
657 db = parse_database_int(&recdb);
658 } else {
659 explain_failure(&recdb, res);
660 _exit(1);
661 }
662
663 switch (recdb.type) {
664 case RECDB_MMAP:
665 #ifdef HAVE_MMAP
666 munmap(recdb.s, recdb.length);
667 #endif
668 case RECDB_FILE:
669 fclose(recdb.f);
670 break;
671 /* Appease gcc */
672 default:
673 break;
674 }
675 return db;
676 }