chiark / gitweb /
Start rationalizing network address configuration.
[disorder] / lib / configuration.c
... / ...
CommitLineData
1/*
2 * This file is part of DisOrder.
3 * Copyright (C) 2004-2009 Richard Kettlewell
4 * Portions copyright (C) 2007 Mark Wooding
5 *
6 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19/** @file lib/configuration.c
20 * @brief Configuration file support
21 */
22
23#include "common.h"
24
25#include <errno.h>
26#include <sys/types.h>
27#include <sys/stat.h>
28#include <unistd.h>
29#include <ctype.h>
30#include <stddef.h>
31#include <pwd.h>
32#include <langinfo.h>
33#include <pcre.h>
34#include <signal.h>
35
36#include "rights.h"
37#include "configuration.h"
38#include "mem.h"
39#include "log.h"
40#include "split.h"
41#include "syscalls.h"
42#include "table.h"
43#include "inputline.h"
44#include "charset.h"
45#include "defs.h"
46#include "printf.h"
47#include "regsub.h"
48#include "signame.h"
49#include "authhash.h"
50#include "vector.h"
51#include "uaudio.h"
52
53/** @brief Path to config file
54 *
55 * set_configfile() sets the deafult if it is null.
56 */
57char *configfile;
58
59/** @brief Read user configuration
60 *
61 * If clear, the user-specific configuration is not read.
62 */
63int config_per_user = 1;
64
65/** @brief Table of audio APIs
66 *
67 * Only set in server processes.
68 */
69const struct uaudio *const *config_uaudio_apis;
70
71/** @brief Config file parser state */
72struct config_state {
73 /** @brief Filename */
74 const char *path;
75 /** @brief Line number */
76 int line;
77 /** @brief Configuration object under construction */
78 struct config *config;
79};
80
81/** @brief Current configuration */
82struct config *config;
83
84/** @brief One configuration item */
85struct conf {
86 /** @brief Name as it appears in the config file */
87 const char *name;
88 /** @brief Offset in @ref config structure */
89 size_t offset;
90 /** @brief Pointer to item type */
91 const struct conftype *type;
92 /** @brief Pointer to item-specific validation routine */
93 int (*validate)(const struct config_state *cs,
94 int nvec, char **vec);
95};
96
97/** @brief Type of a configuration item */
98struct conftype {
99 /** @brief Pointer to function to set item */
100 int (*set)(const struct config_state *cs,
101 const struct conf *whoami,
102 int nvec, char **vec);
103 /** @brief Pointer to function to free item */
104 void (*free)(struct config *c, const struct conf *whoami);
105};
106
107/** @brief Compute the address of an item */
108#define ADDRESS(C, TYPE) ((TYPE *)((char *)(C) + whoami->offset))
109/** @brief Return the value of an item */
110#define VALUE(C, TYPE) (*ADDRESS(C, TYPE))
111
112static int set_signal(const struct config_state *cs,
113 const struct conf *whoami,
114 int nvec, char **vec) {
115 int n;
116
117 if(nvec != 1) {
118 error(0, "%s:%d: '%s' requires one argument",
119 cs->path, cs->line, whoami->name);
120 return -1;
121 }
122 if((n = find_signal(vec[0])) == -1) {
123 error(0, "%s:%d: unknown signal '%s'",
124 cs->path, cs->line, vec[0]);
125 return -1;
126 }
127 VALUE(cs->config, int) = n;
128 return 0;
129}
130
131static int set_collections(const struct config_state *cs,
132 const struct conf *whoami,
133 int nvec, char **vec) {
134 struct collectionlist *cl;
135 const char *root, *encoding, *module;
136
137 switch(nvec) {
138 case 1:
139 module = 0;
140 encoding = 0;
141 root = vec[0];
142 break;
143 case 2:
144 module = vec[0];
145 encoding = 0;
146 root = vec[1];
147 break;
148 case 3:
149 module = vec[0];
150 encoding = vec[1];
151 root = vec[2];
152 break;
153 case 0:
154 error(0, "%s:%d: '%s' requires at least one argument",
155 cs->path, cs->line, whoami->name);
156 return -1;
157 default:
158 error(0, "%s:%d: '%s' requires at most three arguments",
159 cs->path, cs->line, whoami->name);
160 return -1;
161 }
162 /* Sanity check root */
163 if(root[0] != '/') {
164 error(0, "%s:%d: collection root must start with '/'",
165 cs->path, cs->line);
166 return -1;
167 }
168 if(root[1] && root[strlen(root)-1] == '/') {
169 error(0, "%s:%d: collection root must not end with '/'",
170 cs->path, cs->line);
171 return -1;
172 }
173 /* Defaults */
174 if(!module)
175 module = "fs";
176 if(!encoding)
177 encoding = nl_langinfo(CODESET);
178 cl = ADDRESS(cs->config, struct collectionlist);
179 ++cl->n;
180 cl->s = xrealloc(cl->s, cl->n * sizeof (struct collection));
181 cl->s[cl->n - 1].module = xstrdup(module);
182 cl->s[cl->n - 1].encoding = xstrdup(encoding);
183 cl->s[cl->n - 1].root = xstrdup(root);
184 return 0;
185}
186
187static int set_boolean(const struct config_state *cs,
188 const struct conf *whoami,
189 int nvec, char **vec) {
190 int state;
191
192 if(nvec != 1) {
193 error(0, "%s:%d: '%s' takes only one argument",
194 cs->path, cs->line, whoami->name);
195 return -1;
196 }
197 if(!strcmp(vec[0], "yes")) state = 1;
198 else if(!strcmp(vec[0], "no")) state = 0;
199 else {
200 error(0, "%s:%d: argument to '%s' must be 'yes' or 'no'",
201 cs->path, cs->line, whoami->name);
202 return -1;
203 }
204 VALUE(cs->config, int) = state;
205 return 0;
206}
207
208static int set_string(const struct config_state *cs,
209 const struct conf *whoami,
210 int nvec, char **vec) {
211 if(nvec != 1) {
212 error(0, "%s:%d: '%s' takes only one argument",
213 cs->path, cs->line, whoami->name);
214 return -1;
215 }
216 VALUE(cs->config, char *) = xstrdup(vec[0]);
217 return 0;
218}
219
220static int set_stringlist(const struct config_state *cs,
221 const struct conf *whoami,
222 int nvec, char **vec) {
223 int n;
224 struct stringlist *sl;
225
226 sl = ADDRESS(cs->config, struct stringlist);
227 sl->n = 0;
228 for(n = 0; n < nvec; ++n) {
229 sl->n++;
230 sl->s = xrealloc(sl->s, (sl->n * sizeof (char *)));
231 sl->s[sl->n - 1] = xstrdup(vec[n]);
232 }
233 return 0;
234}
235
236static int set_integer(const struct config_state *cs,
237 const struct conf *whoami,
238 int nvec, char **vec) {
239 char *e;
240
241 if(nvec != 1) {
242 error(0, "%s:%d: '%s' takes only one argument",
243 cs->path, cs->line, whoami->name);
244 return -1;
245 }
246 if(xstrtol(ADDRESS(cs->config, long), vec[0], &e, 0)) {
247 error(errno, "%s:%d: converting integer", cs->path, cs->line);
248 return -1;
249 }
250 if(*e) {
251 error(0, "%s:%d: invalid integer syntax", cs->path, cs->line);
252 return -1;
253 }
254 return 0;
255}
256
257static int set_stringlist_accum(const struct config_state *cs,
258 const struct conf *whoami,
259 int nvec, char **vec) {
260 int n;
261 struct stringlist *s;
262 struct stringlistlist *sll;
263
264 sll = ADDRESS(cs->config, struct stringlistlist);
265 if(nvec == 0) {
266 sll->n = 0;
267 return 0;
268 }
269 sll->n++;
270 sll->s = xrealloc(sll->s, (sll->n * sizeof (struct stringlist)));
271 s = &sll->s[sll->n - 1];
272 s->n = nvec;
273 s->s = xmalloc((nvec + 1) * sizeof (char *));
274 for(n = 0; n < nvec; ++n)
275 s->s[n] = xstrdup(vec[n]);
276 return 0;
277}
278
279static int set_string_accum(const struct config_state *cs,
280 const struct conf *whoami,
281 int nvec, char **vec) {
282 int n;
283 struct stringlist *sl;
284
285 sl = ADDRESS(cs->config, struct stringlist);
286 if(nvec == 0) {
287 sl->n = 0;
288 return 0;
289 }
290 for(n = 0; n < nvec; ++n) {
291 sl->n++;
292 sl->s = xrealloc(sl->s, (sl->n * sizeof (char *)));
293 sl->s[sl->n - 1] = xstrdup(vec[n]);
294 }
295 return 0;
296}
297
298static int set_restrict(const struct config_state *cs,
299 const struct conf *whoami,
300 int nvec, char **vec) {
301 unsigned r = 0;
302 int n, i;
303
304 static const struct restriction {
305 const char *name;
306 unsigned bit;
307 } restrictions[] = {
308 { "remove", RESTRICT_REMOVE },
309 { "scratch", RESTRICT_SCRATCH },
310 { "move", RESTRICT_MOVE },
311 };
312
313 for(n = 0; n < nvec; ++n) {
314 if((i = TABLE_FIND(restrictions, name, vec[n])) < 0) {
315 error(0, "%s:%d: invalid restriction '%s'",
316 cs->path, cs->line, vec[n]);
317 return -1;
318 }
319 r |= restrictions[i].bit;
320 }
321 VALUE(cs->config, unsigned) = r;
322 return 0;
323}
324
325static int parse_sample_format(const struct config_state *cs,
326 struct stream_header *format,
327 int nvec, char **vec) {
328 char *p = vec[0];
329 long t;
330
331 if(nvec != 1) {
332 error(0, "%s:%d: wrong number of arguments", cs->path, cs->line);
333 return -1;
334 }
335 if(xstrtol(&t, p, &p, 0)) {
336 error(errno, "%s:%d: converting bits-per-sample", cs->path, cs->line);
337 return -1;
338 }
339 if(t != 8 && t != 16) {
340 error(0, "%s:%d: bad bite-per-sample (%ld)", cs->path, cs->line, t);
341 return -1;
342 }
343 if(format) format->bits = t;
344 switch (*p) {
345 case 'l': case 'L': t = ENDIAN_LITTLE; p++; break;
346 case 'b': case 'B': t = ENDIAN_BIG; p++; break;
347 default: t = ENDIAN_NATIVE; break;
348 }
349 if(format) format->endian = t;
350 if(*p != '/') {
351 error(errno, "%s:%d: expected `/' after bits-per-sample",
352 cs->path, cs->line);
353 return -1;
354 }
355 p++;
356 if(xstrtol(&t, p, &p, 0)) {
357 error(errno, "%s:%d: converting sample-rate", cs->path, cs->line);
358 return -1;
359 }
360 if(t < 1 || t > INT_MAX) {
361 error(0, "%s:%d: silly sample-rate (%ld)", cs->path, cs->line, t);
362 return -1;
363 }
364 if(format) format->rate = t;
365 if(*p != '/') {
366 error(0, "%s:%d: expected `/' after sample-rate",
367 cs->path, cs->line);
368 return -1;
369 }
370 p++;
371 if(xstrtol(&t, p, &p, 0)) {
372 error(errno, "%s:%d: converting channels", cs->path, cs->line);
373 return -1;
374 }
375 if(t < 1 || t > 8) {
376 error(0, "%s:%d: silly number (%ld) of channels", cs->path, cs->line, t);
377 return -1;
378 }
379 if(format) format->channels = t;
380 if(*p) {
381 error(0, "%s:%d: junk after channels", cs->path, cs->line);
382 return -1;
383 }
384 return 0;
385}
386
387static int set_sample_format(const struct config_state *cs,
388 const struct conf *whoami,
389 int nvec, char **vec) {
390 return parse_sample_format(cs, ADDRESS(cs->config, struct stream_header),
391 nvec, vec);
392}
393
394static int set_namepart(const struct config_state *cs,
395 const struct conf *whoami,
396 int nvec, char **vec) {
397 struct namepartlist *npl = ADDRESS(cs->config, struct namepartlist);
398 unsigned reflags;
399 const char *errstr;
400 int erroffset, n;
401 pcre *re;
402
403 if(nvec < 3) {
404 error(0, "%s:%d: namepart needs at least 3 arguments", cs->path, cs->line);
405 return -1;
406 }
407 if(nvec > 5) {
408 error(0, "%s:%d: namepart needs at most 5 arguments", cs->path, cs->line);
409 return -1;
410 }
411 reflags = nvec >= 5 ? regsub_flags(vec[4]) : 0;
412 if(!(re = pcre_compile(vec[1],
413 PCRE_UTF8
414 |regsub_compile_options(reflags),
415 &errstr, &erroffset, 0))) {
416 error(0, "%s:%d: error compiling regexp /%s/: %s (offset %d)",
417 cs->path, cs->line, vec[1], errstr, erroffset);
418 return -1;
419 }
420 npl->s = xrealloc(npl->s, (npl->n + 1) * sizeof (struct namepart));
421 npl->s[npl->n].part = xstrdup(vec[0]);
422 npl->s[npl->n].re = re;
423 npl->s[npl->n].replace = xstrdup(vec[2]);
424 npl->s[npl->n].context = xstrdup(vec[3]);
425 npl->s[npl->n].reflags = reflags;
426 ++npl->n;
427 /* XXX a bit of a bodge; relies on there being very few parts. */
428 for(n = 0; (n < cs->config->nparts
429 && strcmp(cs->config->parts[n], vec[0])); ++n)
430 ;
431 if(n >= cs->config->nparts) {
432 cs->config->parts = xrealloc(cs->config->parts,
433 (cs->config->nparts + 1) * sizeof (char *));
434 cs->config->parts[cs->config->nparts++] = xstrdup(vec[0]);
435 }
436 return 0;
437}
438
439static int set_transform(const struct config_state *cs,
440 const struct conf *whoami,
441 int nvec, char **vec) {
442 struct transformlist *tl = ADDRESS(cs->config, struct transformlist);
443 pcre *re;
444 unsigned reflags;
445 const char *errstr;
446 int erroffset;
447
448 if(nvec < 3) {
449 error(0, "%s:%d: transform needs at least 3 arguments", cs->path, cs->line);
450 return -1;
451 }
452 if(nvec > 5) {
453 error(0, "%s:%d: transform needs at most 5 arguments", cs->path, cs->line);
454 return -1;
455 }
456 reflags = (nvec >= 5 ? regsub_flags(vec[4]) : 0);
457 if(!(re = pcre_compile(vec[1],
458 PCRE_UTF8
459 |regsub_compile_options(reflags),
460 &errstr, &erroffset, 0))) {
461 error(0, "%s:%d: error compiling regexp /%s/: %s (offset %d)",
462 cs->path, cs->line, vec[1], errstr, erroffset);
463 return -1;
464 }
465 tl->t = xrealloc(tl->t, (tl->n + 1) * sizeof (struct namepart));
466 tl->t[tl->n].type = xstrdup(vec[0]);
467 tl->t[tl->n].context = xstrdup(vec[3] ? vec[3] : "*");
468 tl->t[tl->n].re = re;
469 tl->t[tl->n].replace = xstrdup(vec[2]);
470 tl->t[tl->n].flags = reflags;
471 ++tl->n;
472 return 0;
473}
474
475static int set_rights(const struct config_state *cs,
476 const struct conf *whoami,
477 int nvec, char **vec) {
478 if(nvec != 1) {
479 error(0, "%s:%d: '%s' requires one argument",
480 cs->path, cs->line, whoami->name);
481 return -1;
482 }
483 if(parse_rights(vec[0], 0, 1)) {
484 error(0, "%s:%d: invalid rights string '%s'",
485 cs->path, cs->line, vec[0]);
486 return -1;
487 }
488 *ADDRESS(cs->config, char *) = vec[0];
489 return 0;
490}
491
492static int set_netaddress(const struct config_state *cs,
493 const struct conf *whoami,
494 int nvec, char **vec) {
495 struct netaddress *na = ADDRESS(cs->config, struct netaddress);
496
497 if(netaddress_parse(na, nvec, vec)) {
498 error(0, "%s:%d: invalid network address", cs->path, cs->line);
499 return -1;
500 }
501 return 0;
502}
503
504/* free functions */
505
506static void free_none(struct config attribute((unused)) *c,
507 const struct conf attribute((unused)) *whoami) {
508}
509
510static void free_string(struct config *c,
511 const struct conf *whoami) {
512 xfree(VALUE(c, char *));
513}
514
515static void free_stringlist(struct config *c,
516 const struct conf *whoami) {
517 int n;
518 struct stringlist *sl = ADDRESS(c, struct stringlist);
519
520 for(n = 0; n < sl->n; ++n)
521 xfree(sl->s[n]);
522 xfree(sl->s);
523}
524
525static void free_stringlistlist(struct config *c,
526 const struct conf *whoami) {
527 int n, m;
528 struct stringlistlist *sll = ADDRESS(c, struct stringlistlist);
529 struct stringlist *sl;
530
531 for(n = 0; n < sll->n; ++n) {
532 sl = &sll->s[n];
533 for(m = 0; m < sl->n; ++m)
534 xfree(sl->s[m]);
535 xfree(sl->s);
536 }
537 xfree(sll->s);
538}
539
540static void free_collectionlist(struct config *c,
541 const struct conf *whoami) {
542 struct collectionlist *cll = ADDRESS(c, struct collectionlist);
543 struct collection *cl;
544 int n;
545
546 for(n = 0; n < cll->n; ++n) {
547 cl = &cll->s[n];
548 xfree(cl->module);
549 xfree(cl->encoding);
550 xfree(cl->root);
551 }
552 xfree(cll->s);
553}
554
555static void free_namepartlist(struct config *c,
556 const struct conf *whoami) {
557 struct namepartlist *npl = ADDRESS(c, struct namepartlist);
558 struct namepart *np;
559 int n;
560
561 for(n = 0; n < npl->n; ++n) {
562 np = &npl->s[n];
563 xfree(np->part);
564 pcre_free(np->re); /* ...whatever pcre_free is set to. */
565 xfree(np->replace);
566 xfree(np->context);
567 }
568 xfree(npl->s);
569}
570
571static void free_transformlist(struct config *c,
572 const struct conf *whoami) {
573 struct transformlist *tl = ADDRESS(c, struct transformlist);
574 struct transform *t;
575 int n;
576
577 for(n = 0; n < tl->n; ++n) {
578 t = &tl->t[n];
579 xfree(t->type);
580 pcre_free(t->re); /* ...whatever pcre_free is set to. */
581 xfree(t->replace);
582 xfree(t->context);
583 }
584 xfree(tl->t);
585}
586
587static void free_netaddress(struct config *c,
588 const struct conf *whoami) {
589 struct netaddress *na = ADDRESS(c, struct netaddress);
590
591 xfree(na->address);
592}
593
594/* configuration types */
595
596static const struct conftype
597 type_signal = { set_signal, free_none },
598 type_collections = { set_collections, free_collectionlist },
599 type_boolean = { set_boolean, free_none },
600 type_string = { set_string, free_string },
601 type_stringlist = { set_stringlist, free_stringlist },
602 type_integer = { set_integer, free_none },
603 type_stringlist_accum = { set_stringlist_accum, free_stringlistlist },
604 type_string_accum = { set_string_accum, free_stringlist },
605 type_sample_format = { set_sample_format, free_none },
606 type_restrict = { set_restrict, free_none },
607 type_namepart = { set_namepart, free_namepartlist },
608 type_transform = { set_transform, free_transformlist },
609 type_netaddress = { set_netaddress, free_netaddress },
610 type_rights = { set_rights, free_none };
611
612/* specific validation routine */
613
614#define VALIDATE_FILE(test, what) do { \
615 struct stat sb; \
616 int n; \
617 \
618 for(n = 0; n < nvec; ++n) { \
619 if(stat(vec[n], &sb) < 0) { \
620 error(errno, "%s:%d: %s", cs->path, cs->line, vec[n]); \
621 return -1; \
622 } \
623 if(!test(sb.st_mode)) { \
624 error(0, "%s:%d: %s is not a %s", \
625 cs->path, cs->line, vec[n], what); \
626 return -1; \
627 } \
628 } \
629} while(0)
630
631static int validate_isabspath(const struct config_state *cs,
632 int nvec, char **vec) {
633 int n;
634
635 for(n = 0; n < nvec; ++n)
636 if(vec[n][0] != '/') {
637 error(errno, "%s:%d: %s: not an absolute path",
638 cs->path, cs->line, vec[n]);
639 return -1;
640 }
641 return 0;
642}
643
644static int validate_isdir(const struct config_state *cs,
645 int nvec, char **vec) {
646 VALIDATE_FILE(S_ISDIR, "directory");
647 return 0;
648}
649
650static int validate_isreg(const struct config_state *cs,
651 int nvec, char **vec) {
652 VALIDATE_FILE(S_ISREG, "regular file");
653 return 0;
654}
655
656static int validate_player(const struct config_state *cs,
657 int nvec,
658 char attribute((unused)) **vec) {
659 if(nvec < 2) {
660 error(0, "%s:%d: should be at least 'player PATTERN MODULE'",
661 cs->path, cs->line);
662 return -1;
663 }
664 return 0;
665}
666
667static int validate_tracklength(const struct config_state *cs,
668 int nvec,
669 char attribute((unused)) **vec) {
670 if(nvec < 2) {
671 error(0, "%s:%d: should be at least 'tracklength PATTERN MODULE'",
672 cs->path, cs->line);
673 return -1;
674 }
675 return 0;
676}
677
678static int validate_allow(const struct config_state *cs,
679 int nvec,
680 char attribute((unused)) **vec) {
681 if(nvec != 2) {
682 error(0, "%s:%d: must be 'allow NAME PASS'", cs->path, cs->line);
683 return -1;
684 }
685 return 0;
686}
687
688static int validate_non_negative(const struct config_state *cs,
689 int nvec, char **vec) {
690 long n;
691
692 if(nvec < 1) {
693 error(0, "%s:%d: missing argument", cs->path, cs->line);
694 return -1;
695 }
696 if(nvec > 1) {
697 error(0, "%s:%d: too many arguments", cs->path, cs->line);
698 return -1;
699 }
700 if(xstrtol(&n, vec[0], 0, 0)) {
701 error(0, "%s:%d: %s", cs->path, cs->line, strerror(errno));
702 return -1;
703 }
704 if(n < 0) {
705 error(0, "%s:%d: must not be negative", cs->path, cs->line);
706 return -1;
707 }
708 return 0;
709}
710
711static int validate_positive(const struct config_state *cs,
712 int nvec, char **vec) {
713 long n;
714
715 if(nvec < 1) {
716 error(0, "%s:%d: missing argument", cs->path, cs->line);
717 return -1;
718 }
719 if(nvec > 1) {
720 error(0, "%s:%d: too many arguments", cs->path, cs->line);
721 return -1;
722 }
723 if(xstrtol(&n, vec[0], 0, 0)) {
724 error(0, "%s:%d: %s", cs->path, cs->line, strerror(errno));
725 return -1;
726 }
727 if(n <= 0) {
728 error(0, "%s:%d: must be positive", cs->path, cs->line);
729 return -1;
730 }
731 return 0;
732}
733
734static int validate_isauser(const struct config_state *cs,
735 int attribute((unused)) nvec,
736 char **vec) {
737 struct passwd *pw;
738
739 if(!(pw = getpwnam(vec[0]))) {
740 error(0, "%s:%d: no such user as '%s'", cs->path, cs->line, vec[0]);
741 return -1;
742 }
743 return 0;
744}
745
746static int validate_sample_format(const struct config_state *cs,
747 int attribute((unused)) nvec,
748 char **vec) {
749 return parse_sample_format(cs, 0, nvec, vec);
750}
751
752static int validate_any(const struct config_state attribute((unused)) *cs,
753 int attribute((unused)) nvec,
754 char attribute((unused)) **vec) {
755 return 0;
756}
757
758static int validate_url(const struct config_state attribute((unused)) *cs,
759 int attribute((unused)) nvec,
760 char **vec) {
761 const char *s;
762 int n;
763 /* absoluteURI = scheme ":" ( hier_part | opaque_part )
764 scheme = alpha *( alpha | digit | "+" | "-" | "." ) */
765 s = vec[0];
766 n = strspn(s, ("abcdefghijklmnopqrstuvwxyz"
767 "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
768 "0123456789"));
769 if(s[n] != ':') {
770 error(0, "%s:%d: invalid url '%s'", cs->path, cs->line, vec[0]);
771 return -1;
772 }
773 if(!strncmp(s, "http:", 5)
774 || !strncmp(s, "https:", 6)) {
775 s += n + 1;
776 /* we only do a rather cursory check */
777 if(strncmp(s, "//", 2)) {
778 error(0, "%s:%d: invalid url '%s'", cs->path, cs->line, vec[0]);
779 return -1;
780 }
781 }
782 return 0;
783}
784
785static int validate_alias(const struct config_state *cs,
786 int nvec,
787 char **vec) {
788 const char *s;
789 int in_brackets = 0, c;
790
791 if(nvec < 1) {
792 error(0, "%s:%d: missing argument", cs->path, cs->line);
793 return -1;
794 }
795 if(nvec > 1) {
796 error(0, "%s:%d: too many arguments", cs->path, cs->line);
797 return -1;
798 }
799 s = vec[0];
800 while((c = (unsigned char)*s++)) {
801 if(in_brackets) {
802 if(c == '}')
803 in_brackets = 0;
804 else if(!isalnum(c)) {
805 error(0, "%s:%d: invalid part name in alias expansion in '%s'",
806 cs->path, cs->line, vec[0]);
807 return -1;
808 }
809 } else {
810 if(c == '{') {
811 in_brackets = 1;
812 if(*s == '/')
813 ++s;
814 } else if(c == '\\') {
815 if(!(c = (unsigned char)*s++)) {
816 error(0, "%s:%d: unterminated escape in alias expansion in '%s'",
817 cs->path, cs->line, vec[0]);
818 return -1;
819 } else if(c != '\\' && c != '{') {
820 error(0, "%s:%d: invalid escape in alias expansion in '%s'",
821 cs->path, cs->line, vec[0]);
822 return -1;
823 }
824 }
825 }
826 ++s;
827 }
828 if(in_brackets) {
829 error(0, "%s:%d: unterminated part name in alias expansion in '%s'",
830 cs->path, cs->line, vec[0]);
831 return -1;
832 }
833 return 0;
834}
835
836static int validate_addrport(const struct config_state attribute((unused)) *cs,
837 int nvec,
838 char attribute((unused)) **vec) {
839 switch(nvec) {
840 case 0:
841 error(0, "%s:%d: missing address",
842 cs->path, cs->line);
843 return -1;
844 case 1:
845 error(0, "%s:%d: missing port name/number",
846 cs->path, cs->line);
847 return -1;
848 case 2:
849 return 0;
850 default:
851 error(0, "%s:%d: expected ADDRESS PORT",
852 cs->path, cs->line);
853 return -1;
854 }
855}
856
857static int validate_port(const struct config_state attribute((unused)) *cs,
858 int nvec,
859 char attribute((unused)) **vec) {
860 switch(nvec) {
861 case 0:
862 error(0, "%s:%d: missing address",
863 cs->path, cs->line);
864 return -1;
865 case 1:
866 case 2:
867 return 0;
868 default:
869 error(0, "%s:%d: expected [ADDRESS] PORT",
870 cs->path, cs->line);
871 return -1;
872 }
873}
874
875static int validate_algo(const struct config_state attribute((unused)) *cs,
876 int nvec,
877 char **vec) {
878 if(nvec != 1) {
879 error(0, "%s:%d: invalid algorithm specification", cs->path, cs->line);
880 return -1;
881 }
882 if(!valid_authhash(vec[0])) {
883 error(0, "%s:%d: unsuported algorithm '%s'", cs->path, cs->line, vec[0]);
884 return -1;
885 }
886 return 0;
887}
888
889static int validate_backend(const struct config_state attribute((unused)) *cs,
890 int nvec,
891 char **vec) {
892 int n;
893 if(nvec != 1) {
894 error(0, "%s:%d: invalid sound API specification", cs->path, cs->line);
895 return -1;
896 }
897 if(!strcmp(vec[0], "network")) {
898 error(0, "'api network' is deprecated; use 'api rtp'");
899 return 0;
900 }
901 if(config_uaudio_apis) {
902 for(n = 0; config_uaudio_apis[n]; ++n)
903 if(!strcmp(vec[0], config_uaudio_apis[n]->name))
904 return 0;
905 error(0, "%s:%d: unrecognized sound API '%s'", cs->path, cs->line, vec[0]);
906 return -1;
907 }
908 /* In non-server processes we have no idea what's valid */
909 return 0;
910}
911
912static int validate_pausemode(const struct config_state attribute((unused)) *cs,
913 int nvec,
914 char **vec) {
915 if(nvec == 1 && (!strcmp(vec[0], "silence") || !strcmp(vec[0], "suspend")))
916 return 0;
917 error(0, "%s:%d: invalid pause mode", cs->path, cs->line);
918 return -1;
919}
920
921static int validate_destaddr(const struct config_state attribute((unused)) *cs,
922 int nvec,
923 char **vec) {
924 struct netaddress na[1];
925
926 if(netaddress_parse(na, nvec, vec)) {
927 error(0, "%s:%d: invalid network address", cs->path, cs->line);
928 return -1;
929 }
930 if(!na->address) {
931 error(0, "%s:%d: destination address required", cs->path, cs->line);
932 return -1;
933 }
934 return 0;
935}
936
937/** @brief Item name and and offset */
938#define C(x) #x, offsetof(struct config, x)
939/** @brief Item name and and offset */
940#define C2(x,y) #x, offsetof(struct config, y)
941
942/** @brief All configuration items */
943static const struct conf conf[] = {
944 { C(alias), &type_string, validate_alias },
945 { C(allow), &type_stringlist_accum, validate_allow },
946 { C(api), &type_string, validate_backend },
947 { C(authorization_algorithm), &type_string, validate_algo },
948 { C(broadcast), &type_netaddress, validate_destaddr },
949 { C(broadcast_from), &type_netaddress, validate_any },
950 { C(channel), &type_string, validate_any },
951 { C(checkpoint_kbyte), &type_integer, validate_non_negative },
952 { C(checkpoint_min), &type_integer, validate_non_negative },
953 { C(collection), &type_collections, validate_any },
954 { C(connect), &type_stringlist, validate_addrport },
955 { C(cookie_login_lifetime), &type_integer, validate_positive },
956 { C(cookie_key_lifetime), &type_integer, validate_positive },
957 { C(dbversion), &type_integer, validate_positive },
958 { C(default_rights), &type_rights, validate_any },
959 { C(device), &type_string, validate_any },
960 { C(gap), &type_integer, validate_non_negative },
961 { C(history), &type_integer, validate_positive },
962 { C(home), &type_string, validate_isabspath },
963 { C(listen), &type_stringlist, validate_port },
964 { C(lock), &type_boolean, validate_any },
965 { C(mail_sender), &type_string, validate_any },
966 { C(mixer), &type_string, validate_any },
967 { C(multicast_loop), &type_boolean, validate_any },
968 { C(multicast_ttl), &type_integer, validate_non_negative },
969 { C(namepart), &type_namepart, validate_any },
970 { C(new_bias), &type_integer, validate_positive },
971 { C(new_bias_age), &type_integer, validate_positive },
972 { C(new_max), &type_integer, validate_positive },
973 { C2(nice, nice_rescan), &type_integer, validate_non_negative },
974 { C(nice_rescan), &type_integer, validate_non_negative },
975 { C(nice_server), &type_integer, validate_any },
976 { C(nice_speaker), &type_integer, validate_any },
977 { C(noticed_history), &type_integer, validate_positive },
978 { C(password), &type_string, validate_any },
979 { C(pause_mode), &type_string, validate_pausemode },
980 { C(player), &type_stringlist_accum, validate_player },
981 { C(plugins), &type_string_accum, validate_isdir },
982 { C(prefsync), &type_integer, validate_positive },
983 { C(queue_pad), &type_integer, validate_positive },
984 { C(replay_min), &type_integer, validate_non_negative },
985 { C(refresh), &type_integer, validate_positive },
986 { C(reminder_interval), &type_integer, validate_positive },
987 { C(remote_userman), &type_boolean, validate_any },
988 { C2(restrict, restrictions), &type_restrict, validate_any },
989 { C(rtp_delay_threshold), &type_integer, validate_positive },
990 { C(sample_format), &type_sample_format, validate_sample_format },
991 { C(scratch), &type_string_accum, validate_isreg },
992 { C(sendmail), &type_string, validate_isabspath },
993 { C(short_display), &type_integer, validate_positive },
994 { C(signal), &type_signal, validate_any },
995 { C(smtp_server), &type_string, validate_any },
996 { C(sox_generation), &type_integer, validate_non_negative },
997 { C2(speaker_backend, api), &type_string, validate_backend },
998 { C(speaker_command), &type_string, validate_any },
999 { C(stopword), &type_string_accum, validate_any },
1000 { C(templates), &type_string_accum, validate_isdir },
1001 { C(tracklength), &type_stringlist_accum, validate_tracklength },
1002 { C(transform), &type_transform, validate_any },
1003 { C(trust), &type_string_accum, validate_any },
1004 { C(url), &type_string, validate_url },
1005 { C(user), &type_string, validate_isauser },
1006 { C(username), &type_string, validate_any },
1007};
1008
1009/** @brief Find a configuration item's definition by key */
1010static const struct conf *find(const char *key) {
1011 int n;
1012
1013 if((n = TABLE_FIND(conf, name, key)) < 0)
1014 return 0;
1015 return &conf[n];
1016}
1017
1018/** @brief Set a new configuration value */
1019static int config_set(const struct config_state *cs,
1020 int nvec, char **vec) {
1021 const struct conf *which;
1022
1023 D(("config_set %s", vec[0]));
1024 if(!(which = find(vec[0]))) {
1025 error(0, "%s:%d: unknown configuration key '%s'",
1026 cs->path, cs->line, vec[0]);
1027 return -1;
1028 }
1029 return (which->validate(cs, nvec - 1, vec + 1)
1030 || which->type->set(cs, which, nvec - 1, vec + 1));
1031}
1032
1033static int config_set_args(const struct config_state *cs,
1034 const char *which, ...) {
1035 va_list ap;
1036 struct vector v[1];
1037 char *s;
1038
1039 vector_init(v);
1040 vector_append(v, (char *)which);
1041 va_start(ap, which);
1042 while((s = va_arg(ap, char *)))
1043 vector_append(v, s);
1044 va_end(ap);
1045 vector_terminate(v);
1046 return config_set(cs, v->nvec, v->vec);
1047}
1048
1049/** @brief Error callback used by config_include() */
1050static void config_error(const char *msg, void *u) {
1051 const struct config_state *cs = u;
1052
1053 error(0, "%s:%d: %s", cs->path, cs->line, msg);
1054}
1055
1056/** @brief Include a file by name */
1057static int config_include(struct config *c, const char *path) {
1058 FILE *fp;
1059 char *buffer, *inputbuffer, **vec;
1060 int n, ret = 0;
1061 struct config_state cs;
1062
1063 cs.path = path;
1064 cs.line = 0;
1065 cs.config = c;
1066 D(("%s: reading configuration", path));
1067 if(!(fp = fopen(path, "r"))) {
1068 error(errno, "error opening %s", path);
1069 return -1;
1070 }
1071 while(!inputline(path, fp, &inputbuffer, '\n')) {
1072 ++cs.line;
1073 if(!(buffer = mb2utf8(inputbuffer))) {
1074 error(errno, "%s:%d: cannot convert to UTF-8", cs.path, cs.line);
1075 ret = -1;
1076 xfree(inputbuffer);
1077 continue;
1078 }
1079 xfree(inputbuffer);
1080 if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES,
1081 config_error, &cs))) {
1082 ret = -1;
1083 xfree(buffer);
1084 continue;
1085 }
1086 if(n) {
1087 if(!strcmp(vec[0], "include")) {
1088 if(n != 2) {
1089 error(0, "%s:%d: must be 'include PATH'", cs.path, cs.line);
1090 ret = -1;
1091 } else
1092 config_include(c, vec[1]);
1093 } else
1094 ret |= config_set(&cs, n, vec);
1095 }
1096 for(n = 0; vec[n]; ++n) xfree(vec[n]);
1097 xfree(vec);
1098 xfree(buffer);
1099 }
1100 if(ferror(fp)) {
1101 error(errno, "error reading %s", path);
1102 ret = -1;
1103 }
1104 fclose(fp);
1105 return ret;
1106}
1107
1108static const char *const default_stopwords[] = {
1109 "stopword",
1110
1111 "01",
1112 "02",
1113 "03",
1114 "04",
1115 "05",
1116 "06",
1117 "07",
1118 "08",
1119 "09",
1120 "1",
1121 "10",
1122 "11",
1123 "12",
1124 "13",
1125 "14",
1126 "15",
1127 "16",
1128 "17",
1129 "18",
1130 "19",
1131 "2",
1132 "20",
1133 "21",
1134 "22",
1135 "23",
1136 "24",
1137 "25",
1138 "26",
1139 "27",
1140 "28",
1141 "29",
1142 "3",
1143 "30",
1144 "4",
1145 "5",
1146 "6",
1147 "7",
1148 "8",
1149 "9",
1150 "a",
1151 "am",
1152 "an",
1153 "and",
1154 "as",
1155 "for",
1156 "i",
1157 "im",
1158 "in",
1159 "is",
1160 "of",
1161 "on",
1162 "the",
1163 "to",
1164 "too",
1165 "we",
1166};
1167#define NDEFAULT_STOPWORDS (sizeof default_stopwords / sizeof *default_stopwords)
1168
1169static const char *const default_players[] = {
1170 "*.ogg",
1171 "*.flac",
1172 "*.mp3",
1173 "*.wav",
1174};
1175#define NDEFAULT_PLAYERS (sizeof default_players / sizeof *default_players)
1176
1177/** @brief Make a new default configuration */
1178static struct config *config_default(void) {
1179 struct config *c = xmalloc(sizeof *c);
1180 const char *logname;
1181 struct passwd *pw;
1182 struct config_state cs;
1183 size_t n;
1184
1185 cs.path = "<internal>";
1186 cs.line = 0;
1187 cs.config = c;
1188 /* Strings had better be xstrdup'd as they will get freed at some point. */
1189 c->gap = 0;
1190 c->history = 60;
1191 c->home = xstrdup(pkgstatedir);
1192 if(!(pw = getpwuid(getuid())))
1193 fatal(0, "cannot determine our username");
1194 logname = pw->pw_name;
1195 c->username = xstrdup(logname);
1196 c->refresh = 15;
1197 c->prefsync = 3600;
1198 c->signal = SIGKILL;
1199 c->alias = xstrdup("{/artist}{/album}{/title}{ext}");
1200 c->lock = 1;
1201 c->device = xstrdup("default");
1202 c->nice_rescan = 10;
1203 c->speaker_command = 0;
1204 c->sample_format.bits = 16;
1205 c->sample_format.rate = 44100;
1206 c->sample_format.channels = 2;
1207 c->sample_format.endian = ENDIAN_NATIVE;
1208 c->queue_pad = 10;
1209 c->replay_min = 8 * 3600;
1210 c->api = NULL;
1211 c->multicast_ttl = 1;
1212 c->multicast_loop = 1;
1213 c->authorization_algorithm = xstrdup("sha1");
1214 c->noticed_history = 31;
1215 c->short_display = 32;
1216 c->mixer = 0;
1217 c->channel = 0;
1218 c->dbversion = 2;
1219 c->cookie_login_lifetime = 86400;
1220 c->cookie_key_lifetime = 86400 * 7;
1221 if(sendmail_binary[0] && strcmp(sendmail_binary, "none"))
1222 c->sendmail = xstrdup(sendmail_binary);
1223 c->smtp_server = xstrdup("127.0.0.1");
1224 c->new_max = 100;
1225 c->reminder_interval = 600; /* 10m */
1226 c->new_bias_age = 7 * 86400; /* 1 week */
1227 c->new_bias = 4500000; /* 50 times the base weight */
1228 c->sox_generation = DEFAULT_SOX_GENERATION;
1229 /* Default stopwords */
1230 if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
1231 exit(1);
1232 /* Default player configuration */
1233 for(n = 0; n < NDEFAULT_PLAYERS; ++n) {
1234 if(config_set_args(&cs, "player",
1235 default_players[n], "execraw", "disorder-decode", (char *)0))
1236 exit(1);
1237 if(config_set_args(&cs, "tracklength",
1238 default_players[n], "disorder-tracklength", (char *)0))
1239 exit(1);
1240 }
1241 c->broadcast.af = -1;
1242 c->broadcast_from.af = -1;
1243 return c;
1244}
1245
1246char *config_get_file2(struct config *c, const char *name) {
1247 char *s;
1248
1249 byte_xasprintf(&s, "%s/%s", c->home, name);
1250 return s;
1251}
1252
1253/** @brief Set the default configuration file */
1254static void set_configfile(void) {
1255 if(!configfile)
1256 byte_xasprintf(&configfile, "%s/config", pkgconfdir);
1257}
1258
1259/** @brief Free a configuration object */
1260static void config_free(struct config *c) {
1261 int n;
1262
1263 if(c) {
1264 for(n = 0; n < (int)(sizeof conf / sizeof *conf); ++n)
1265 conf[n].type->free(c, &conf[n]);
1266 for(n = 0; n < c->nparts; ++n)
1267 xfree(c->parts[n]);
1268 xfree(c->parts);
1269 xfree(c);
1270 }
1271}
1272
1273/** @brief Set post-parse defaults */
1274static void config_postdefaults(struct config *c,
1275 int server) {
1276 struct config_state cs;
1277 const struct conf *whoami;
1278 int n;
1279
1280 static const char *namepart[][4] = {
1281 { "title", "/([0-9]+ *[-:]? *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display" },
1282 { "title", "/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort" },
1283 { "album", "/([^/]+)/[^/]+$", "$1", "*" },
1284 { "artist", "/([^/]+)/[^/]+/[^/]+$", "$1", "*" },
1285 { "ext", "(\\.[a-zA-Z0-9]+)$", "$1", "*" },
1286 };
1287#define NNAMEPART (int)(sizeof namepart / sizeof *namepart)
1288
1289 static const char *transform[][5] = {
1290 { "track", "^.*/([0-9]+ *[-:]? *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display", "" },
1291 { "track", "^.*/([^/]+)\\.[a-zA-Z0-9]+$", "$1", "sort", "" },
1292 { "dir", "^.*/([^/]+)$", "$1", "*", "" },
1293 { "dir", "^(the) ([^/]*)", "$2, $1", "sort", "i", },
1294 { "dir", "[[:punct:]]", "", "sort", "g", }
1295 };
1296#define NTRANSFORM (int)(sizeof transform / sizeof *transform)
1297
1298 cs.path = "<internal>";
1299 cs.line = 0;
1300 cs.config = c;
1301 if(!c->namepart.n) {
1302 whoami = find("namepart");
1303 for(n = 0; n < NNAMEPART; ++n)
1304 set_namepart(&cs, whoami, 4, (char **)namepart[n]);
1305 }
1306 if(!c->transform.n) {
1307 whoami = find("transform");
1308 for(n = 0; n < NTRANSFORM; ++n)
1309 set_transform(&cs, whoami, 5, (char **)transform[n]);
1310 }
1311 if(!c->api) {
1312 if(c->speaker_command)
1313 c->api = xstrdup("command");
1314 else if(c->broadcast.af != -1)
1315 c->api = xstrdup("rtp");
1316 else if(config_uaudio_apis)
1317 c->api = xstrdup(config_uaudio_apis[0]->name);
1318 else
1319 c->api = xstrdup("<none>");
1320 }
1321 if(!strcmp(c->api, "network"))
1322 c->api = xstrdup("rtp");
1323 if(server) {
1324 if(!strcmp(c->api, "command") && !c->speaker_command)
1325 fatal(0, "'api command' but speaker_command is not set");
1326 if((!strcmp(c->api, "rtp")) && c->broadcast.af == -1)
1327 fatal(0, "'api rtp' but broadcast is not set");
1328 }
1329 /* Override sample format */
1330 if(!strcmp(c->api, "rtp")) {
1331 c->sample_format.rate = 44100;
1332 c->sample_format.channels = 2;
1333 c->sample_format.bits = 16;
1334 c->sample_format.endian = ENDIAN_NATIVE;
1335 }
1336 if(!strcmp(c->api, "coreaudio")) {
1337 c->sample_format.rate = 44100;
1338 c->sample_format.channels = 2;
1339 c->sample_format.bits = 16;
1340 c->sample_format.endian = ENDIAN_NATIVE;
1341 }
1342 if(!c->default_rights) {
1343 rights_type r = RIGHTS__MASK & ~(RIGHT_ADMIN|RIGHT_REGISTER
1344 |RIGHT_MOVE__MASK
1345 |RIGHT_SCRATCH__MASK
1346 |RIGHT_REMOVE__MASK);
1347 /* The idea is to approximate the meaning of the old 'restrict' directive
1348 * in the default rights if they are not overridden. */
1349 if(c->restrictions & RESTRICT_SCRATCH)
1350 r |= RIGHT_SCRATCH_MINE|RIGHT_SCRATCH_RANDOM;
1351 else
1352 r |= RIGHT_SCRATCH_ANY;
1353 if(!(c->restrictions & RESTRICT_MOVE))
1354 r |= RIGHT_MOVE_ANY;
1355 if(c->restrictions & RESTRICT_REMOVE)
1356 r |= RIGHT_REMOVE_MINE;
1357 else
1358 r |= RIGHT_REMOVE_ANY;
1359 c->default_rights = rights_string(r);
1360 }
1361}
1362
1363/** @brief (Re-)read the config file
1364 * @param server If set, do extra checking
1365 */
1366int config_read(int server) {
1367 struct config *c;
1368 char *privconf;
1369 struct passwd *pw;
1370
1371 set_configfile();
1372 c = config_default();
1373 /* standalone Disobedience installs might not have a global config file */
1374 if(access(configfile, F_OK) == 0)
1375 if(config_include(c, configfile))
1376 return -1;
1377 /* if we can read the private config file, do */
1378 if((privconf = config_private())
1379 && access(privconf, R_OK) == 0
1380 && config_include(c, privconf))
1381 return -1;
1382 xfree(privconf);
1383 /* if there's a per-user system config file for this user, read it */
1384 if(config_per_user) {
1385 if(!(pw = getpwuid(getuid())))
1386 fatal(0, "cannot determine our username");
1387 if((privconf = config_usersysconf(pw))
1388 && access(privconf, F_OK) == 0
1389 && config_include(c, privconf))
1390 return -1;
1391 xfree(privconf);
1392 /* if we have a password file, read it */
1393 if((privconf = config_userconf(0, pw))
1394 && access(privconf, F_OK) == 0
1395 && config_include(c, privconf))
1396 return -1;
1397 xfree(privconf);
1398 }
1399 /* install default namepart and transform settings */
1400 config_postdefaults(c, server);
1401 /* everything is good so we shall use the new config */
1402 config_free(config);
1403 /* warn about obsolete directives */
1404 if(c->restrictions)
1405 error(0, "'restrict' will be removed in a future version");
1406 if(c->allow.n)
1407 error(0, "'allow' will be removed in a future version");
1408 if(c->trust.n)
1409 error(0, "'trust' will be removed in a future version");
1410 config = c;
1411 return 0;
1412}
1413
1414/** @brief Return the path to the private configuration file */
1415char *config_private(void) {
1416 char *s;
1417
1418 set_configfile();
1419 byte_xasprintf(&s, "%s.private", configfile);
1420 return s;
1421}
1422
1423/** @brief Return the path to user's personal configuration file */
1424char *config_userconf(const char *home, const struct passwd *pw) {
1425 char *s;
1426
1427 if(!home && !pw && !(pw = getpwuid(getuid())))
1428 fatal(0, "cannot determine our username");
1429 byte_xasprintf(&s, "%s/.disorder/passwd", home ? home : pw->pw_dir);
1430 return s;
1431}
1432
1433/** @brief Return the path to user-specific system configuration */
1434char *config_usersysconf(const struct passwd *pw) {
1435 char *s;
1436
1437 set_configfile();
1438 if(!strchr(pw->pw_name, '/')) {
1439 byte_xasprintf(&s, "%s.%s", configfile, pw->pw_name);
1440 return s;
1441 } else
1442 return 0;
1443}
1444
1445char *config_get_file(const char *name) {
1446 return config_get_file2(config, name);
1447}
1448
1449/*
1450Local Variables:
1451c-basic-offset:2
1452comment-column:40
1453fill-column:79
1454End:
1455*/