2 * Master Configuration routines.
4 * This file contains the common parts of the Bacula
5 * configuration routines.
7 * Note, the configuration file parser consists of three parts
9 * 1. The generic lexical scanner in lib/lex.c and lib/lex.h
11 * 2. The generic config scanner in lib/parse_conf.c and
13 * These files contain the parser code, some utility
14 * routines, and the common store routines (name, int,
15 * string, time, int64, size, ...).
17 * 3. The daemon specific file, which contains the Resource
18 * definitions as well as any specific store routines
19 * for the resource records.
21 * N.B. This is a two pass parser, so if you malloc() a string
22 * in a "store" routine, you must ensure to do it during
23 * only one of the two passes, or to free it between.
24 * Also, note that the resource record is malloced and
25 * saved in save_resource() during pass 1. Anything that
26 * you want saved after pass two (e.g. resource pointers)
27 * must explicitly be done in save_resource. Take a look
28 * at the Job resource in src/dird/dird_conf.c to see how
31 * Kern Sibbald, January MM
36 Copyright (C) 2000-2006 Kern Sibbald
38 This program is free software; you can redistribute it and/or
39 modify it under the terms of the GNU General Public License
40 version 2 as amended with additional clauses defined in the
41 file LICENSE in the main source directory.
43 This program is distributed in the hope that it will be useful,
44 but WITHOUT ANY WARRANTY; without even the implied warranty of
45 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
46 the file LICENSE for additional details.
53 extern int debug_level;
55 /* Each daemon has a slightly different set of
56 * resources, so it will define the following
61 extern RES_TABLE resources[];
62 extern RES **res_head;
65 // work around visual studio name manling preventing external linkage since res_all
66 // is declared as a different type when instantiated.
67 extern "C" CURES res_all;
71 extern int res_all_size;
73 extern brwlock_t res_lock; /* resource lock */
76 /* Forward referenced subroutines */
77 static void scan_types(LEX *lc, MSGS *msg, int dest, char *where, char *cmd);
80 /* Common Resource definitions */
82 /* Message resource directives
83 * name handler value code flags default_value
85 RES_ITEM msgs_items[] = {
86 {"name", store_name, ITEM(res_msgs.hdr.name), 0, 0, 0},
87 {"description", store_str, ITEM(res_msgs.hdr.desc), 0, 0, 0},
88 {"mailcommand", store_str, ITEM(res_msgs.mail_cmd), 0, 0, 0},
89 {"operatorcommand", store_str, ITEM(res_msgs.operator_cmd), 0, 0, 0},
90 {"syslog", store_msgs, ITEM(res_msgs), MD_SYSLOG, 0, 0},
91 {"mail", store_msgs, ITEM(res_msgs), MD_MAIL, 0, 0},
92 {"mailonerror", store_msgs, ITEM(res_msgs), MD_MAIL_ON_ERROR, 0, 0},
93 {"file", store_msgs, ITEM(res_msgs), MD_FILE, 0, 0},
94 {"append", store_msgs, ITEM(res_msgs), MD_APPEND, 0, 0},
95 {"stdout", store_msgs, ITEM(res_msgs), MD_STDOUT, 0, 0},
96 {"stderr", store_msgs, ITEM(res_msgs), MD_STDERR, 0, 0},
97 {"director", store_msgs, ITEM(res_msgs), MD_DIRECTOR, 0, 0},
98 {"console", store_msgs, ITEM(res_msgs), MD_CONSOLE, 0, 0},
99 {"operator", store_msgs, ITEM(res_msgs), MD_OPERATOR, 0, 0},
100 {NULL, NULL, {0}, 0, 0, 0}
107 /* Various message types */
108 static struct s_mtypes msg_types[] = {
113 {"warning", M_WARNING},
116 {"notsaved", M_NOTSAVED},
117 {"skipped", M_SKIPPED},
119 {"terminate", M_TERM},
120 {"restored", M_RESTORED},
121 {"security", M_SECURITY},
127 /* Used for certain KeyWord tables */
134 * Tape Label types permitted in Pool records
136 * tape label label code = token
138 static s_kw tapelabels[] = {
139 {"bacula", B_BACULA_LABEL},
140 {"ansi", B_ANSI_LABEL},
141 {"ibm", B_IBM_LABEL},
146 /* Simply print a message */
147 static void prtmsg(void *sock, const char *fmt, ...)
151 va_start(arg_ptr, fmt);
152 vfprintf(stdout, fmt, arg_ptr);
156 const char *res_to_str(int rcode)
158 if (rcode < r_first || rcode > r_last) {
159 return _("***UNKNOWN***");
161 return resources[rcode-r_first].name;
167 * Initialize the static structure to zeros, then
168 * apply all the default values.
170 void init_resource(int type, RES_ITEM *items, int pass)
173 int rindex = type - r_first;
174 static bool first = true;
177 if (first && (errstat=rwl_init(&res_lock)) != 0) {
178 Emsg1(M_ABORT, 0, _("Unable to initialize resource lock. ERR=%s\n"),
183 memset(&res_all, 0, res_all_size);
184 res_all.hdr.rcode = type;
185 res_all.hdr.refcnt = 1;
187 /* Set defaults in each item */
188 for (i=0; items[i].name; i++) {
189 Dmsg3(900, "Item=%s def=%s defval=%d\n", items[i].name,
190 (items[i].flags & ITEM_DEFAULT) ? "yes" : "no",
191 items[i].default_value);
192 if (items[i].flags & ITEM_DEFAULT && items[i].default_value != 0) {
193 if (items[i].handler == store_bit) {
194 *(int *)(items[i].value) |= items[i].code;
195 } else if (items[i].handler == store_bool) {
196 *(bool *)(items[i].value) = items[i].default_value;
197 } else if (items[i].handler == store_pint ||
198 items[i].handler == store_int) {
199 *(int *)(items[i].value) = items[i].default_value;
200 } else if (items[i].handler == store_int64) {
201 *(int64_t *)(items[i].value) = items[i].default_value;
202 } else if (items[i].handler == store_size) {
203 *(uint64_t *)(items[i].value) = (uint64_t)items[i].default_value;
204 } else if (items[i].handler == store_time) {
205 *(utime_t *)(items[i].value) = (utime_t)items[i].default_value;
206 } else if (pass == 1 && items[i].handler == store_addresses) {
207 init_default_addresses((dlist**)items[i].value, items[i].default_value);
210 /* If this triggers, take a look at lib/parse_conf.h */
211 if (i >= MAX_RES_ITEMS) {
212 Emsg1(M_ERROR_TERM, 0, _("Too many items in %s resource\n"), resources[rindex]);
218 /* Store Messages Destination information */
219 void store_msgs(LEX *lc, RES_ITEM *item, int index, int pass)
226 Dmsg2(900, "store_msgs pass=%d code=%d\n", pass, item->code);
228 switch (item->code) {
231 case MD_SYSLOG: /* syslog */
233 scan_types(lc, (MSGS *)(item->value), item->code, NULL, NULL);
235 case MD_OPERATOR: /* send to operator */
236 case MD_DIRECTOR: /* send to Director */
237 case MD_MAIL: /* mail */
238 case MD_MAIL_ON_ERROR: /* mail if Job errors */
239 if (item->code == MD_OPERATOR) {
240 cmd = res_all.res_msgs.operator_cmd;
242 cmd = res_all.res_msgs.mail_cmd;
244 dest = get_pool_memory(PM_MESSAGE);
247 /* Pick up comma separated list of destinations */
249 token = lex_get_token(lc, T_NAME); /* scan destination */
250 dest = check_pool_memory_size(dest, dest_len + lc->str_len + 2);
252 pm_strcat(dest, " "); /* separate multiple destinations with space */
255 pm_strcat(dest, lc->str);
256 dest_len += lc->str_len;
257 Dmsg2(900, "store_msgs newdest=%s: dest=%s:\n", lc->str, NPRT(dest));
258 token = lex_get_token(lc, T_SKIP_EOL);
259 if (token == T_COMMA) {
260 continue; /* get another destination */
262 if (token != T_EQUALS) {
263 scan_err1(lc, _("expected an =, got: %s"), lc->str);
267 Dmsg1(900, "mail_cmd=%s\n", NPRT(cmd));
268 scan_types(lc, (MSGS *)(item->value), item->code, dest, cmd);
269 free_pool_memory(dest);
270 Dmsg0(900, "done with dest codes\n");
272 case MD_FILE: /* file */
273 case MD_APPEND: /* append */
274 dest = get_pool_memory(PM_MESSAGE);
275 /* Pick up a single destination */
276 token = lex_get_token(lc, T_NAME); /* scan destination */
277 pm_strcpy(dest, lc->str);
278 dest_len = lc->str_len;
279 token = lex_get_token(lc, T_SKIP_EOL);
280 Dmsg1(900, "store_msgs dest=%s:\n", NPRT(dest));
281 if (token != T_EQUALS) {
282 scan_err1(lc, _("expected an =, got: %s"), lc->str);
284 scan_types(lc, (MSGS *)(item->value), item->code, dest, NULL);
285 free_pool_memory(dest);
286 Dmsg0(900, "done with dest codes\n");
290 scan_err1(lc, _("Unknown item code: %d\n"), item->code);
295 set_bit(index, res_all.hdr.item_present);
296 Dmsg0(900, "Done store_msgs\n");
300 * Scan for message types and add them to the message
301 * destination. The basic job here is to connect message types
302 * (WARNING, ERROR, FATAL, INFO, ...) with an appropriate
303 * destination (MAIL, FILE, OPERATOR, ...)
305 static void scan_types(LEX *lc, MSGS *msg, int dest_code, char *where, char *cmd)
313 lex_get_token(lc, T_NAME); /* expect at least one type */
315 if (lc->str[0] == '!') {
322 for (i=0; msg_types[i].name; i++) {
323 if (strcasecmp(str, msg_types[i].name) == 0) {
324 msg_type = msg_types[i].token;
330 scan_err1(lc, _("message type: %s not found"), str);
334 if (msg_type == M_MAX+1) { /* all? */
335 for (i=1; i<=M_MAX; i++) { /* yes set all types */
336 add_msg_dest(msg, dest_code, i, where, cmd);
339 rem_msg_dest(msg, dest_code, msg_type, where);
341 add_msg_dest(msg, dest_code, msg_type, where, cmd);
346 Dmsg0(900, "call lex_get_token() to eat comma\n");
347 lex_get_token(lc, T_ALL); /* eat comma */
349 Dmsg0(900, "Done scan_types()\n");
354 * This routine is ONLY for resource names
355 * Store a name at specified address.
357 void store_name(LEX *lc, RES_ITEM *item, int index, int pass)
359 POOLMEM *msg = get_pool_memory(PM_EMSG);
360 lex_get_token(lc, T_NAME);
361 if (!is_name_valid(lc->str, &msg)) {
362 scan_err1(lc, "%s\n", msg);
364 free_pool_memory(msg);
365 /* Store the name both pass 1 and pass 2 */
366 if (*(item->value)) {
367 scan_err2(lc, _("Attempt to redefine name \"%s\" to \"%s\"."),
368 *(item->value), lc->str);
370 *(item->value) = bstrdup(lc->str);
372 set_bit(index, res_all.hdr.item_present);
377 * Store a name string at specified address
378 * A name string is limited to MAX_RES_NAME_LENGTH
380 void store_strname(LEX *lc, RES_ITEM *item, int index, int pass)
382 lex_get_token(lc, T_NAME);
385 *(item->value) = bstrdup(lc->str);
388 set_bit(index, res_all.hdr.item_present);
391 /* Store a string at specified address */
392 void store_str(LEX *lc, RES_ITEM *item, int index, int pass)
394 lex_get_token(lc, T_STRING);
396 *(item->value) = bstrdup(lc->str);
399 set_bit(index, res_all.hdr.item_present);
403 * Store a directory name at specified address. Note, we do
404 * shell expansion except if the string begins with a vertical
405 * bar (i.e. it will likely be passed to the shell later).
407 void store_dir(LEX *lc, RES_ITEM *item, int index, int pass)
409 lex_get_token(lc, T_STRING);
411 if (lc->str[0] != '|') {
412 do_shell_expansion(lc->str, sizeof(lc->str));
414 *(item->value) = bstrdup(lc->str);
417 set_bit(index, res_all.hdr.item_present);
421 /* Store a password specified address in MD5 coding */
422 void store_password(LEX *lc, RES_ITEM *item, int index, int pass)
425 struct MD5Context md5c;
426 unsigned char digest[CRYPTO_DIGEST_MD5_SIZE];
430 lex_get_token(lc, T_STRING);
433 MD5Update(&md5c, (unsigned char *) (lc->str), lc->str_len);
434 MD5Final(digest, &md5c);
435 for (i = j = 0; i < sizeof(digest); i++) {
436 sprintf(&sig[j], "%02x", digest[i]);
439 *(item->value) = bstrdup(sig);
442 set_bit(index, res_all.hdr.item_present);
446 /* Store a resource at specified address.
447 * If we are in pass 2, do a lookup of the
450 void store_res(LEX *lc, RES_ITEM *item, int index, int pass)
454 lex_get_token(lc, T_NAME);
456 res = GetResWithName(item->code, lc->str);
458 scan_err3(lc, _("Could not find config Resource %s referenced on line %d : %s\n"),
459 lc->str, lc->line_no, lc->line);
461 if (*(item->value)) {
462 scan_err3(lc, _("Attempt to redefine resource \"%s\" referenced on line %d : %s\n"),
463 item->name, lc->line_no, lc->line);
465 *(item->value) = (char *)res;
468 set_bit(index, res_all.hdr.item_present);
472 * Store a resource pointer in an alist. default_value indicates how many
473 * times this routine can be called -- i.e. how many alists
475 * If we are in pass 2, do a lookup of the
478 void store_alist_res(LEX *lc, RES_ITEM *item, int index, int pass)
481 int count = item->default_value;
486 if (count == 0) { /* always store in item->value */
488 if ((item->value)[i] == NULL) {
489 list = New(alist(10, not_owned_by_alist));
491 list = (alist *)(item->value)[i];
494 /* Find empty place to store this directive */
495 while ((item->value)[i] != NULL && i++ < count) { }
497 scan_err4(lc, _("Too many %s directives. Max. is %d. line %d: %s\n"),
498 lc->str, count, lc->line_no, lc->line);
500 list = New(alist(10, not_owned_by_alist));
504 lex_get_token(lc, T_NAME); /* scan next item */
505 res = GetResWithName(item->code, lc->str);
507 scan_err3(lc, _("Could not find config Resource \"%s\" referenced on line %d : %s\n"),
508 item->name, lc->line_no, lc->line);
510 Dmsg5(900, "Append %p to alist %p size=%d i=%d %s\n",
511 res, list, list->size(), i, item->name);
513 (item->value)[i] = (char *)list;
514 if (lc->ch != ',') { /* if no other item follows */
517 lex_get_token(lc, T_ALL); /* eat comma */
521 set_bit(index, res_all.hdr.item_present);
526 * Store a string in an alist.
528 void store_alist_str(LEX *lc, RES_ITEM *item, int index, int pass)
533 if (*(item->value) == NULL) {
534 list = New(alist(10, owned_by_alist));
536 list = (alist *)(*(item->value));
539 lex_get_token(lc, T_STRING); /* scan next item */
540 Dmsg4(900, "Append %s to alist %p size=%d %s\n",
541 lc->str, list, list->size(), item->name);
542 list->append(bstrdup(lc->str));
543 *(item->value) = (char *)list;
546 set_bit(index, res_all.hdr.item_present);
552 * Store default values for Resource from xxxDefs
553 * If we are in pass 2, do a lookup of the
554 * resource and store everything not explicitly set
557 * Note, here item points to the main resource (e.g. Job, not
558 * the jobdefs, which we look up).
560 void store_defs(LEX *lc, RES_ITEM *item, int index, int pass)
564 lex_get_token(lc, T_NAME);
566 Dmsg2(900, "Code=%d name=%s\n", item->code, lc->str);
567 res = GetResWithName(item->code, lc->str);
569 scan_err3(lc, _("Missing config Resource \"%s\" referenced on line %d : %s\n"),
570 lc->str, lc->line_no, lc->line);
578 /* Store an integer at specified address */
579 void store_int(LEX *lc, RES_ITEM *item, int index, int pass)
581 lex_get_token(lc, T_INT32);
582 *(int *)(item->value) = lc->int32_val;
584 set_bit(index, res_all.hdr.item_present);
587 /* Store a positive integer at specified address */
588 void store_pint(LEX *lc, RES_ITEM *item, int index, int pass)
590 lex_get_token(lc, T_PINT32);
591 *(int *)(item->value) = lc->pint32_val;
593 set_bit(index, res_all.hdr.item_present);
597 /* Store an 64 bit integer at specified address */
598 void store_int64(LEX *lc, RES_ITEM *item, int index, int pass)
600 lex_get_token(lc, T_INT64);
601 *(int64_t *)(item->value) = lc->int64_val;
603 set_bit(index, res_all.hdr.item_present);
606 /* Store a size in bytes */
607 void store_size(LEX *lc, RES_ITEM *item, int index, int pass)
613 Dmsg0(900, "Enter store_size\n");
614 token = lex_get_token(lc, T_SKIP_EOL);
619 case T_UNQUOTED_STRING:
620 bstrncpy(bsize, lc->str, sizeof(bsize)); /* save first part */
621 /* if terminated by space, scan and get modifier */
622 while (lc->ch == ' ') {
623 token = lex_get_token(lc, T_ALL);
627 case T_UNQUOTED_STRING:
628 bstrncat(bsize, lc->str, sizeof(bsize));
632 if (!size_to_uint64(bsize, strlen(bsize), &uvalue)) {
633 scan_err1(lc, _("expected a size number, got: %s"), lc->str);
635 *(uint64_t *)(item->value) = uvalue;
638 scan_err1(lc, _("expected a size, got: %s"), lc->str);
641 if (token != T_EOL) {
644 set_bit(index, res_all.hdr.item_present);
645 Dmsg0(900, "Leave store_size\n");
649 /* Store a time period in seconds */
650 void store_time(LEX *lc, RES_ITEM *item, int index, int pass)
656 token = lex_get_token(lc, T_SKIP_EOL);
661 case T_UNQUOTED_STRING:
662 bstrncpy(period, lc->str, sizeof(period)); /* get first part */
663 /* if terminated by space, scan and get modifier */
664 while (lc->ch == ' ') {
665 token = lex_get_token(lc, T_ALL);
669 case T_UNQUOTED_STRING:
670 bstrncat(period, lc->str, sizeof(period));
674 if (!duration_to_utime(period, &utime)) {
675 scan_err1(lc, _("expected a time period, got: %s"), period);
677 *(utime_t *)(item->value) = utime;
680 scan_err1(lc, _("expected a time period, got: %s"), lc->str);
683 if (token != T_EOL) {
686 set_bit(index, res_all.hdr.item_present);
690 /* Store a yes/no in a bit field */
691 void store_bit(LEX *lc, RES_ITEM *item, int index, int pass)
693 lex_get_token(lc, T_NAME);
694 if (strcasecmp(lc->str, "yes") == 0 || strcasecmp(lc->str, "true") == 0) {
695 *(int *)(item->value) |= item->code;
696 } else if (strcasecmp(lc->str, "no") == 0 || strcasecmp(lc->str, "false") == 0) {
697 *(int *)(item->value) &= ~(item->code);
699 scan_err2(lc, _("Expect %s, got: %s"), "YES, NO, TRUE, or FALSE", lc->str); /* YES and NO must not be translated */
702 set_bit(index, res_all.hdr.item_present);
705 /* Store a bool in a bit field */
706 void store_bool(LEX *lc, RES_ITEM *item, int index, int pass)
708 lex_get_token(lc, T_NAME);
709 if (strcasecmp(lc->str, "yes") == 0 || strcasecmp(lc->str, "true") == 0) {
710 *(bool *)(item->value) = true;
711 } else if (strcasecmp(lc->str, "no") == 0 || strcasecmp(lc->str, "false") == 0) {
712 *(bool *)(item->value) = false;
714 scan_err2(lc, _("Expect %s, got: %s"), "YES, NO, TRUE, or FALSE", lc->str); /* YES and NO must not be translated */
717 set_bit(index, res_all.hdr.item_present);
722 * Store Tape Label Type (Bacula, ANSI, IBM)
725 void store_label(LEX *lc, RES_ITEM *item, int index, int pass)
729 token = lex_get_token(lc, T_NAME);
730 /* Store the label pass 2 so that type is defined */
731 for (i=0; tapelabels[i].name; i++) {
732 if (strcasecmp(lc->str, tapelabels[i].name) == 0) {
733 *(int *)(item->value) = tapelabels[i].token;
739 scan_err1(lc, _("Expected a Tape Label keyword, got: %s"), lc->str);
742 set_bit(index, res_all.hdr.item_present);
752 /*********************************************************************
754 * Parse configuration file
756 * Return 0 if reading failed, 1 otherwise
757 * Note, the default behavior unless you have set an alternate
758 * scan_error handler is to die on an error.
761 parse_config(const char *cf, LEX_ERROR_HANDLER *scan_error, int err_type)
766 enum parse_state state = p_none;
767 RES_ITEM *items = NULL;
770 /* Make two passes. The first builds the name symbol table,
771 * and the second picks up the items.
773 Dmsg0(900, "Enter parse_config()\n");
774 for (pass=1; pass <= 2; pass++) {
775 Dmsg1(900, "parse_config pass %d\n", pass);
776 if ((lc = lex_open_file(lc, cf, scan_error)) == NULL) {
778 /* We must create a lex packet to print the error */
779 lc = (LEX *)malloc(sizeof(LEX));
780 memset(lc, 0, sizeof(LEX));
782 lc->scan_error = scan_error;
784 lex_set_default_error_handler(lc);
786 lex_set_error_handler_error_type(lc, err_type) ;
787 bstrncpy(lc->str, cf, sizeof(lc->str));
789 scan_err2(lc, _("Cannot open config file \"%s\": %s\n"),
790 lc->str, be.strerror());
794 lex_set_error_handler_error_type(lc, err_type) ;
795 while ((token=lex_get_token(lc, T_ALL)) != T_EOF) {
796 Dmsg1(900, "parse got token=%s\n", lex_tok_to_str(token));
799 if (token == T_EOL) {
802 if (token != T_IDENTIFIER) {
803 scan_err1(lc, _("Expected a Resource name identifier, got: %s"), lc->str);
806 for (i=0; resources[i].name; i++)
807 if (strcasecmp(resources[i].name, lc->str) == 0) {
809 items = resources[i].items;
810 res_type = resources[i].rcode;
811 init_resource(res_type, items, pass);
814 if (state == p_none) {
815 scan_err1(lc, _("expected resource name, got: %s"), lc->str);
826 scan_err1(lc, _("not in resource definition: %s"), lc->str);
829 for (i=0; items[i].name; i++) {
830 if (strcasecmp(items[i].name, lc->str) == 0) {
831 /* If the ITEM_NO_EQUALS flag is set we do NOT
832 * scan for = after the keyword */
833 if (!(items[i].flags & ITEM_NO_EQUALS)) {
834 token = lex_get_token(lc, T_SKIP_EOL);
835 Dmsg1 (900, "in T_IDENT got token=%s\n", lex_tok_to_str(token));
836 if (token != T_EQUALS) {
837 scan_err1(lc, _("expected an equals, got: %s"), lc->str);
841 Dmsg1(800, "calling handler for %s\n", items[i].name);
842 /* Call item handler */
843 items[i].handler(lc, &items[i], i, pass);
849 Dmsg2(900, "level=%d id=%s\n", level, lc->str);
850 Dmsg1(900, "Keyword = %s\n", lc->str);
851 scan_err1(lc, _("Keyword \"%s\" not permitted in this resource.\n"
852 "Perhaps you left the trailing brace off of the previous resource."), lc->str);
860 Dmsg0(900, "T_EOB => define new resource\n");
861 if (res_all.hdr.name == NULL) {
862 scan_err0(lc, _("Name not specified for resource"));
864 save_resource(res_type, items, pass); /* save resource */
871 scan_err2(lc, _("unexpected token %d %s in resource definition"),
872 token, lex_tok_to_str(token));
877 scan_err1(lc, _("Unknown parser state %d\n"), state);
881 if (state != p_none) {
882 scan_err0(lc, _("End of conf file reached with unclosed resource."));
885 if (debug_level >= 900 && pass == 2) {
887 for (i=r_first; i<=r_last; i++) {
888 dump_resource(i, res_head[i-r_first], prtmsg, NULL);
891 lc = lex_close_file(lc);
893 Dmsg0(900, "Leave parse_config()\n");
897 /*********************************************************************
899 * Free configuration resources
902 void free_config_resources()
904 for (int i=r_first; i<=r_last; i++) {
905 free_resource(res_head[i-r_first], i);
906 res_head[i-r_first] = NULL;
910 RES **save_config_resources()
912 int num = r_last - r_first + 1;
913 RES **res = (RES **)malloc(num*sizeof(RES *));
914 for (int i=0; i<num; i++) {
915 res[i] = res_head[i];
923 int size = (r_last - r_first + 1) * sizeof(RES *);
924 RES **res = (RES **)malloc(size);
925 memset(res, 0, size);