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