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