1 /* recdb.c - recursive/record database implementation
2 * Copyright 2000-2004 srvx Development Team
4 * This file is part of x3.
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.
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.
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.
27 #ifdef HAVE_SYS_MMAN_H
30 #ifdef HAVE_SYS_STAT_H
35 #define MMAP_MAP_LENGTH (getpagesize()*1024)
37 /* file format (grammar in Backus-Naur Form):
40 * record := qstring [ '=' ] ( qstring | object | stringlist ) ';'
41 * qstring := '"' ( [^\\] | ('\\' [\\n]) )* '"'
42 * object := '{' record* '}'
43 * stringlist := '(' [ qstring [',' qstring]* ] ')'
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
52 struct recdb_context
{
63 typedef struct recdb_file
{
65 FILE *f
; /* For RECDB_FILE, RECDB_MMAP */
66 char *s
; /* For RECDB_STRING, RECDB_MMAP */
67 enum recdb_filetype type
;
70 struct recdb_context ctx
;
74 typedef struct recdb_outfile
{
75 FILE *f
; /* For RECDB_FILE, RECDB_MMAP */
76 char *s
; /* For RECDB_STRING, RECDB_MMAP */
78 struct { /* For RECDB_STRING */
82 struct { /* For RECDB_MMAP */
87 enum recdb_filetype type
;
96 static int mmap_error
;
102 #define ABORT(recdb, code, ch) longjmp((recdb)->env, ((code) << 8) | (ch))
105 ABORT(RECDB
*recdb
, int code
, unsigned char ch
) {
106 longjmp(recdb
->env
, code
<< 8 | ch
);
116 EXPECTED_START_RECORD_DATA
,
121 static void parse_record_int(RECDB
*recdb
, char **pname
, struct record_data
**prd
);
123 /* allocation functions */
125 #define alloc_record_data_int() malloc(sizeof(struct record_data))
128 alloc_record_data_qstring(const char *string
)
130 struct record_data
*rd
;
131 rd
= alloc_record_data_int();
132 SET_RECORD_QSTRING(rd
, string
);
137 alloc_record_data_object(dict_t obj
)
139 struct record_data
*rd
;
140 rd
= alloc_record_data_int();
141 SET_RECORD_OBJECT(rd
, obj
);
146 alloc_record_data_string_list(struct string_list
*slist
)
148 struct record_data
*rd
;
149 rd
= alloc_record_data_int();
150 SET_RECORD_STRING_LIST(rd
, slist
);
155 alloc_string_list(int size
)
157 struct string_list
*slist
;
158 slist
= malloc(sizeof(struct string_list
));
161 slist
->list
= slist
->size
? malloc(size
*sizeof(char*)) : NULL
;
168 dict_t db
= dict_new();
169 dict_set_free_data(db
, free_record_data
);
173 /* misc. operations */
176 string_list_append(struct string_list
*slist
, char *string
)
178 if (slist
->used
== slist
->size
) {
181 slist
->list
= realloc(slist
->list
, slist
->size
*sizeof(char*));
184 slist
->list
= malloc(slist
->size
*sizeof(char*));
187 slist
->list
[slist
->used
++] = string
;
191 string_list_copy(struct string_list
*slist
)
193 struct string_list
*new_list
;
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
]);
203 int slist_compare_two(const void *pa
, const void *pb
)
205 return irccasecmp(*(const char**)pa
, *(const char **)pb
);
209 string_list_sort(struct string_list
*slist
)
211 qsort(slist
->list
, slist
->used
, sizeof(slist
->list
[0]), slist_compare_two
);
215 database_get_path(dict_t db
, const char *path
)
217 char *new_path
= strdup(path
), *orig_path
= new_path
;
219 struct record_data
*rd
;
221 for (part
=new_path
; *new_path
; new_path
++) {
222 if (*new_path
!= '/') continue;
225 rd
= dict_find(db
, part
, NULL
);
226 if (!rd
|| rd
->type
!= RECDB_OBJECT
) {
235 rd
= dict_find(db
, part
, NULL
);
241 database_get_data(dict_t db
, const char *path
, enum recdb_type type
)
243 assert(path
!= NULL
);
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
;
254 free_string_list(struct string_list
*slist
)
259 for (i
=0; i
<slist
->used
; i
++)
260 free(slist
->list
[i
]);
266 free_record_data(void *rdata
)
268 struct record_data
*r
= rdata
;
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;
278 /* parse functions */
283 switch (recdb
->type
) {
285 return feof(recdb
->f
);
291 return ((size_t)recdb
->pos
>= recdb
->length
);
303 switch (recdb
->type
) {
305 res
= fgetc(recdb
->f
);
309 res
= dbeof(recdb
) ? EOF
: recdb
->s
[recdb
->pos
++];
315 if (res
== EOL
) recdb
->ctx
.line
++, recdb
->ctx
.col
=1;
316 else if (res
!= EOF
) recdb
->ctx
.col
++;
321 dbungetc(int c
, RECDB
*recdb
)
323 switch (recdb
->type
) {
329 recdb
->s
[--recdb
->pos
] = c
;
332 if (c
== EOL
) recdb
->ctx
.line
--, recdb
->ctx
.col
=-1;
333 else recdb
->ctx
.col
--;
336 /* returns first non-whitespace, non-comment character (-1 for EOF found) */
338 parse_skip_ws(RECDB
*recdb
)
340 int c
, d
, in_comment
= 0;
341 while (!dbeof(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 */
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 */
359 } while (c
!= EOF
&& c
!= EOL
);
361 if (d
!= EOF
) dbungetc(d
, recdb
);
369 parse_qstring(RECDB
*recdb
)
372 int used
=0, size
=8, c
;
373 struct recdb_context start_ctx
;
376 if ((c
= parse_skip_ws(recdb
)) == EOF
) return NULL
;
377 start_ctx
= recdb
->ctx
;
378 if (c
!= '"') ABORT(recdb
, EXPECTED_OPEN_QUOTE
, c
);
380 while (!dbeof(recdb
) && (c
= dbgetc(recdb
)) != '"') {
382 /* There should never be a literal newline, as it is saved as a \n */
385 ABORT(recdb
, UNTERMINATED_STRING
, ' ');
389 switch (c
= dbgetc(recdb
)) {
390 case '0': /* \<octal>, 000 through 377 */
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') {
406 if ((c
= dbgetc(recdb
)) == EOF
) {
409 if ((c
< '0') || (c
> '7')) {
416 c
= (int)strtol(digits
, NULL
, 8);
425 char digits
[3] = { '\0', '\0', '\0' };
426 for (i
=0; i
< 2; i
++) {
427 if ((c
= dbgetc(recdb
)) == EOF
) {
437 c
= (int)strtol(digits
, NULL
, 16);
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;
459 buff
= realloc(buff
, size
);
462 if (c
!= '"' && dbeof(recdb
)) {
464 recdb
->ctx
= start_ctx
;
465 ABORT(recdb
, UNTERMINATED_STRING
, EOF
);
472 parse_object(RECDB
*recdb
)
476 struct record_data
*rd
;
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;
486 parse_record_int(recdb
, &name
, &rd
);
487 dict_insert(obj
, name
, rd
);
493 parse_string_list(RECDB
*recdb
)
495 struct string_list
*slist
;
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);
501 c
= parse_skip_ws(recdb
);
502 if (c
== EOF
|| c
== ')') break;
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
);
513 parse_record_int(RECDB
*recdb
, char **pname
, struct record_data
**prd
)
516 *pname
= parse_qstring(recdb
);
517 c
= parse_skip_ws(recdb
);
521 ABORT(recdb
, EXPECTED_RECORD_DATA
, EOF
);
523 if (c
== '=') c
= parse_skip_ws(recdb
);
525 *prd
= malloc(sizeof(**prd
));
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
);
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
);
536 if ((c
= parse_skip_ws(recdb
)) != ';') ABORT(recdb
, EXPECTED_SEMICOLON
, c
);
540 parse_database_int(RECDB
*recdb
)
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
);
554 failure_reason(int code
)
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";
568 if (code
== -1) reason
= "Premature end of file";
573 explain_failure(RECDB
*recdb
, int code
)
575 log_module(MAIN_LOG
, LOG_ERROR
, "%s (got '%c') at %s line %d column %d.",
576 failure_reason(code
), code
& 255,
577 recdb
->source
, recdb
->ctx
.line
, recdb
->ctx
.col
);
581 parse_record(const char *text
, char **pname
, struct record_data
**prd
)
587 recdb
.source
= "<user-supplied text>";
589 recdb
.s
= strdup(text
);
590 recdb
.length
= strlen(text
);
592 recdb
.type
= RECDB_STRING
;
593 recdb
.ctx
.line
= recdb
.ctx
.col
= 1;
594 if ((res
= setjmp(recdb
.env
)) == 0) {
595 parse_record_int(&recdb
, pname
, prd
);
600 return failure_reason(res
);
605 parse_database(const char *filename
)
610 struct stat statinfo
;
612 recdb
.source
= filename
;
613 if (!(recdb
.f
= fopen(filename
, "r"))) {
614 log_module(MAIN_LOG
, LOG_ERROR
, "Unable to open database file '%s' for reading: %s", filename
, strerror(errno
));
618 if (fstat(fileno(recdb
.f
), &statinfo
)) {
619 log_module(MAIN_LOG
, LOG_ERROR
, "Unable to fstat database file '%s': %s", filename
, strerror(errno
));
622 recdb
.length
= (size_t)statinfo
.st_size
;
623 if (recdb
.length
== 0) {
624 return alloc_database();
629 if (!mmap_error
&& (recdb
.s
= mmap(NULL
, recdb
.length
, PROT_READ
|PROT_WRITE
, MAP_PRIVATE
, fileno(recdb
.f
), 0)) != MAP_FAILED
) {
630 recdb
.type
= RECDB_MMAP
;
631 madvise(recdb
.s
, recdb
.length
, MADV_SEQUENTIAL
);
633 /* Fall back to stdio */
635 log_module(MAIN_LOG
, LOG_WARNING
, "Unable to mmap database file '%s' (falling back to stdio): %s", filename
, strerror(errno
));
642 recdb
.type
= RECDB_FILE
;
645 recdb
.ctx
.line
= recdb
.ctx
.col
= 1;
648 if ((res
= setjmp(recdb
.env
)) == 0) {
649 db
= parse_database_int(&recdb
);
651 explain_failure(&recdb
, res
);
655 switch (recdb
.type
) {
658 munmap(recdb
.s
, recdb
.length
);