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