chiark / gitweb /
Implement most of the playlist commands in the server. playlist-set
[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(playlist_lock_timeout), &type_integer,     validate_positive },
956   { C(playlist_max) ,    &type_integer,          validate_positive },
957   { C(plugins),          &type_string_accum,     validate_isdir },
958   { C(prefsync),         &type_integer,          validate_positive },
959   { C(queue_pad),        &type_integer,          validate_positive },
960   { C(replay_min),       &type_integer,          validate_non_negative },
961   { C(refresh),          &type_integer,          validate_positive },
962   { C(reminder_interval), &type_integer,         validate_positive },
963   { C(remote_userman),   &type_boolean,          validate_any },
964   { C2(restrict, restrictions),         &type_restrict,         validate_any },
965   { C(sample_format),    &type_sample_format,    validate_sample_format },
966   { C(scratch),          &type_string_accum,     validate_isreg },
967   { C(sendmail),         &type_string,           validate_isabspath },
968   { C(short_display),    &type_integer,          validate_positive },
969   { C(signal),           &type_signal,           validate_any },
970   { C(smtp_server),      &type_string,           validate_any },
971   { C(sox_generation),   &type_integer,          validate_non_negative },
972   { C2(speaker_backend, api),  &type_backend,          validate_any },
973   { C(speaker_command),  &type_string,           validate_any },
974   { C(stopword),         &type_string_accum,     validate_any },
975   { C(templates),        &type_string_accum,     validate_isdir },
976   { C(tracklength),      &type_stringlist_accum, validate_tracklength },
977   { C(transform),        &type_transform,        validate_any },
978   { C(trust),            &type_string_accum,     validate_any },
979   { C(url),              &type_string,           validate_url },
980   { C(user),             &type_string,           validate_isauser },
981   { C(username),         &type_string,           validate_any },
982 };
983
984 /** @brief Find a configuration item's definition by key */
985 static const struct conf *find(const char *key) {
986   int n;
987
988   if((n = TABLE_FIND(conf, name, key)) < 0)
989     return 0;
990   return &conf[n];
991 }
992
993 /** @brief Set a new configuration value */
994 static int config_set(const struct config_state *cs,
995                       int nvec, char **vec) {
996   const struct conf *which;
997
998   D(("config_set %s", vec[0]));
999   if(!(which = find(vec[0]))) {
1000     error(0, "%s:%d: unknown configuration key '%s'",
1001           cs->path, cs->line, vec[0]);
1002     return -1;
1003   }
1004   return (which->validate(cs, nvec - 1, vec + 1)
1005           || which->type->set(cs, which, nvec - 1, vec + 1));
1006 }
1007
1008 static int config_set_args(const struct config_state *cs,
1009                            const char *which, ...) {
1010   va_list ap;
1011   struct vector v[1];
1012   char *s;
1013
1014   vector_init(v);
1015   vector_append(v, (char *)which);
1016   va_start(ap, which);
1017   while((s = va_arg(ap, char *)))
1018     vector_append(v, s);
1019   va_end(ap);
1020   vector_terminate(v);
1021   return config_set(cs, v->nvec, v->vec);
1022 }
1023
1024 /** @brief Error callback used by config_include() */
1025 static void config_error(const char *msg, void *u) {
1026   const struct config_state *cs = u;
1027
1028   error(0, "%s:%d: %s", cs->path, cs->line, msg);
1029 }
1030
1031 /** @brief Include a file by name */
1032 static int config_include(struct config *c, const char *path) {
1033   FILE *fp;
1034   char *buffer, *inputbuffer, **vec;
1035   int n, ret = 0;
1036   struct config_state cs;
1037
1038   cs.path = path;
1039   cs.line = 0;
1040   cs.config = c;
1041   D(("%s: reading configuration", path));
1042   if(!(fp = fopen(path, "r"))) {
1043     error(errno, "error opening %s", path);
1044     return -1;
1045   }
1046   while(!inputline(path, fp, &inputbuffer, '\n')) {
1047     ++cs.line;
1048     if(!(buffer = mb2utf8(inputbuffer))) {
1049       error(errno, "%s:%d: cannot convert to UTF-8", cs.path, cs.line);
1050       ret = -1;
1051       xfree(inputbuffer);
1052       continue;
1053     }
1054     xfree(inputbuffer);
1055     if(!(vec = split(buffer, &n, SPLIT_COMMENTS|SPLIT_QUOTES,
1056                      config_error, &cs))) {
1057       ret = -1;
1058       xfree(buffer);
1059       continue;
1060     }
1061     if(n) {
1062       if(!strcmp(vec[0], "include")) {
1063         if(n != 2) {
1064           error(0, "%s:%d: must be 'include PATH'", cs.path, cs.line);
1065           ret = -1;
1066         } else
1067           config_include(c, vec[1]);
1068       } else
1069         ret |= config_set(&cs, n, vec);
1070     }
1071     for(n = 0; vec[n]; ++n) xfree(vec[n]);
1072     xfree(vec);
1073     xfree(buffer);
1074   }
1075   if(ferror(fp)) {
1076     error(errno, "error reading %s", path);
1077     ret = -1;
1078   }
1079   fclose(fp);
1080   return ret;
1081 }
1082
1083 static const char *const default_stopwords[] = {
1084   "stopword",
1085
1086   "01",
1087   "02",
1088   "03",
1089   "04",
1090   "05",
1091   "06",
1092   "07",
1093   "08",
1094   "09",
1095   "1",
1096   "10",
1097   "11",
1098   "12",
1099   "13",
1100   "14",
1101   "15",
1102   "16",
1103   "17",
1104   "18",
1105   "19",
1106   "2",
1107   "20",
1108   "21",
1109   "22",
1110   "23",
1111   "24",
1112   "25",
1113   "26",
1114   "27",
1115   "28",
1116   "29",
1117   "3",
1118   "30",
1119   "4",
1120   "5",
1121   "6",
1122   "7",
1123   "8",
1124   "9",
1125   "a",
1126   "am",
1127   "an",
1128   "and",
1129   "as",
1130   "for",
1131   "i",
1132   "im",
1133   "in",
1134   "is",
1135   "of",
1136   "on",
1137   "the",
1138   "to",
1139   "too",
1140   "we",
1141 };
1142 #define NDEFAULT_STOPWORDS (sizeof default_stopwords / sizeof *default_stopwords)
1143
1144 static const char *const default_players[] = {
1145   "*.ogg",
1146   "*.flac",
1147   "*.mp3",
1148   "*.wav",
1149 };
1150 #define NDEFAULT_PLAYERS (sizeof default_players / sizeof *default_players)
1151
1152 /** @brief Make a new default configuration */
1153 static struct config *config_default(void) {
1154   struct config *c = xmalloc(sizeof *c);
1155   const char *logname;
1156   struct passwd *pw;
1157   struct config_state cs;
1158   size_t n;
1159
1160   cs.path = "<internal>";
1161   cs.line = 0;
1162   cs.config = c;
1163   /* Strings had better be xstrdup'd as they will get freed at some point. */
1164   c->gap = 0;
1165   c->history = 60;
1166   c->home = xstrdup(pkgstatedir);
1167   if(!(pw = getpwuid(getuid())))
1168     fatal(0, "cannot determine our username");
1169   logname = pw->pw_name;
1170   c->username = xstrdup(logname);
1171   c->refresh = 15;
1172   c->prefsync = 3600;
1173   c->signal = SIGKILL;
1174   c->alias = xstrdup("{/artist}{/album}{/title}{ext}");
1175   c->lock = 1;
1176   c->device = xstrdup("default");
1177   c->nice_rescan = 10;
1178   c->speaker_command = 0;
1179   c->sample_format.bits = 16;
1180   c->sample_format.rate = 44100;
1181   c->sample_format.channels = 2;
1182   c->sample_format.endian = ENDIAN_NATIVE;
1183   c->queue_pad = 10;
1184   c->replay_min = 8 * 3600;
1185   c->api = -1;
1186   c->multicast_ttl = 1;
1187   c->multicast_loop = 1;
1188   c->authorization_algorithm = xstrdup("sha1");
1189   c->noticed_history = 31;
1190   c->short_display = 32;
1191   c->mixer = 0;
1192   c->channel = 0;
1193   c->dbversion = 2;
1194   c->cookie_login_lifetime = 86400;
1195   c->cookie_key_lifetime = 86400 * 7;
1196   if(sendmail_binary[0] && strcmp(sendmail_binary, "none"))
1197     c->sendmail = xstrdup(sendmail_binary);
1198   c->smtp_server = xstrdup("127.0.0.1");
1199   c->new_max = 100;
1200   c->reminder_interval = 600;           /* 10m */
1201   c->new_bias_age = 7 * 86400;          /* 1 week */
1202   c->new_bias = 9000000;                /* 100 times the base weight */
1203   c->playlist_max = INT_MAX;            /* effectively no limit */
1204   c->playlist_lock_timeout = 10;        /* 10s */
1205   /* Default stopwords */
1206   if(config_set(&cs, (int)NDEFAULT_STOPWORDS, (char **)default_stopwords))
1207     exit(1);
1208   /* Default player configuration */
1209   for(n = 0; n < NDEFAULT_PLAYERS; ++n) {
1210     if(config_set_args(&cs, "player",
1211                        default_players[n], "execraw", "disorder-decode", (char *)0))
1212       exit(1);
1213     if(config_set_args(&cs, "tracklength",
1214                        default_players[n], "disorder-tracklength", (char *)0))
1215       exit(1);
1216   }
1217   return c;
1218 }
1219
1220 char *config_get_file2(struct config *c, const char *name) {
1221   char *s;
1222
1223   byte_xasprintf(&s, "%s/%s", c->home, name);
1224   return s;
1225 }
1226
1227 /** @brief Set the default configuration file */
1228 static void set_configfile(void) {
1229   if(!configfile)
1230     byte_xasprintf(&configfile, "%s/config", pkgconfdir);
1231 }
1232
1233 /** @brief Free a configuration object */
1234 static void config_free(struct config *c) {
1235   int n;
1236
1237   if(c) {
1238     for(n = 0; n < (int)(sizeof conf / sizeof *conf); ++n)
1239       conf[n].type->free(c, &conf[n]);
1240     for(n = 0; n < c->nparts; ++n)
1241       xfree(c->parts[n]);
1242     xfree(c->parts);
1243     xfree(c);
1244   }
1245 }
1246
1247 /** @brief Set post-parse defaults */
1248 static void config_postdefaults(struct config *c,
1249                                 int server) {
1250   struct config_state cs;
1251   const struct conf *whoami;
1252   int n;
1253
1254   static const char *namepart[][4] = {
1255     { "title",  "/([0-9]+ *[-:] *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display" },
1256     { "title",  "/([^/]+)\\.[a-zA-Z0-9]+$",           "$1", "sort" },
1257     { "album",  "/([^/]+)/[^/]+$",                    "$1", "*" },
1258     { "artist", "/([^/]+)/[^/]+/[^/]+$",              "$1", "*" },
1259     { "ext",    "(\\.[a-zA-Z0-9]+)$",                 "$1", "*" },
1260   };
1261 #define NNAMEPART (int)(sizeof namepart / sizeof *namepart)
1262
1263   static const char *transform[][5] = {
1264     { "track", "^.*/([0-9]+ *[-:] *)?([^/]+)\\.[a-zA-Z0-9]+$", "$2", "display", "" },
1265     { "track", "^.*/([^/]+)\\.[a-zA-Z0-9]+$",           "$1", "sort", "" },
1266     { "dir",   "^.*/([^/]+)$",                          "$1", "*", "" },
1267     { "dir",   "^(the) ([^/]*)",                        "$2, $1", "sort", "i", },
1268     { "dir",   "[[:punct:]]",                           "", "sort", "g", }
1269   };
1270 #define NTRANSFORM (int)(sizeof transform / sizeof *transform)
1271
1272   cs.path = "<internal>";
1273   cs.line = 0;
1274   cs.config = c;
1275   if(!c->namepart.n) {
1276     whoami = find("namepart");
1277     for(n = 0; n < NNAMEPART; ++n)
1278       set_namepart(&cs, whoami, 4, (char **)namepart[n]);
1279   }
1280   if(!c->transform.n) {
1281     whoami = find("transform");
1282     for(n = 0; n < NTRANSFORM; ++n)
1283       set_transform(&cs, whoami, 5, (char **)transform[n]);
1284   }
1285   if(c->api == -1) {
1286     if(c->speaker_command)
1287       c->api = BACKEND_COMMAND;
1288     else if(c->broadcast.n)
1289       c->api = BACKEND_NETWORK;
1290     else
1291       c->api = DEFAULT_BACKEND;
1292   }
1293   if(server) {
1294     if(c->api == BACKEND_COMMAND && !c->speaker_command)
1295       fatal(0, "'api command' but speaker_command is not set");
1296     if(c->api == BACKEND_NETWORK && !c->broadcast.n)
1297       fatal(0, "'api network' but broadcast is not set");
1298   }
1299   /* Override sample format */
1300   switch(c->api) {
1301   case BACKEND_NETWORK:
1302     c->sample_format.rate = 44100;
1303     c->sample_format.channels = 2;
1304     c->sample_format.bits = 16;
1305     c->sample_format.endian = ENDIAN_BIG;
1306     break;
1307   case BACKEND_COREAUDIO:
1308     c->sample_format.rate = 44100;
1309     c->sample_format.channels = 2;
1310     c->sample_format.bits = 16;
1311     c->sample_format.endian = ENDIAN_NATIVE;
1312     break; 
1313   }
1314   if(!c->default_rights) {
1315     rights_type r = RIGHTS__MASK & ~(RIGHT_ADMIN|RIGHT_REGISTER
1316                                      |RIGHT_MOVE__MASK
1317                                      |RIGHT_SCRATCH__MASK
1318                                      |RIGHT_REMOVE__MASK);
1319     /* The idea is to approximate the meaning of the old 'restrict' directive
1320      * in the default rights if they are not overridden. */
1321     if(c->restrictions & RESTRICT_SCRATCH)
1322       r |= RIGHT_SCRATCH_MINE|RIGHT_SCRATCH_RANDOM;
1323     else
1324       r |= RIGHT_SCRATCH_ANY;
1325     if(!(c->restrictions & RESTRICT_MOVE))
1326       r |= RIGHT_MOVE_ANY;
1327     if(c->restrictions & RESTRICT_REMOVE)
1328       r |= RIGHT_REMOVE_MINE;
1329     else
1330       r |= RIGHT_REMOVE_ANY;
1331     c->default_rights = rights_string(r);
1332   }
1333 }
1334
1335 /** @brief (Re-)read the config file
1336  * @param server If set, do extra checking
1337  */
1338 int config_read(int server) {
1339   struct config *c;
1340   char *privconf;
1341   struct passwd *pw;
1342
1343   set_configfile();
1344   c = config_default();
1345   /* standalone Disobedience installs might not have a global config file */
1346   if(access(configfile, F_OK) == 0)
1347     if(config_include(c, configfile))
1348       return -1;
1349   /* if we can read the private config file, do */
1350   if((privconf = config_private())
1351      && access(privconf, R_OK) == 0
1352      && config_include(c, privconf))
1353     return -1;
1354   xfree(privconf);
1355   /* if there's a per-user system config file for this user, read it */
1356   if(config_per_user) {
1357     if(!(pw = getpwuid(getuid())))
1358       fatal(0, "cannot determine our username");
1359     if((privconf = config_usersysconf(pw))
1360        && access(privconf, F_OK) == 0
1361        && config_include(c, privconf))
1362       return -1;
1363     xfree(privconf);
1364     /* if we have a password file, read it */
1365     if((privconf = config_userconf(0, pw))
1366        && access(privconf, F_OK) == 0
1367        && config_include(c, privconf))
1368       return -1;
1369     xfree(privconf);
1370   }
1371   /* install default namepart and transform settings */
1372   config_postdefaults(c, server);
1373   /* everything is good so we shall use the new config */
1374   config_free(config);
1375   /* warn about obsolete directives */
1376   if(c->restrictions)
1377     error(0, "'restrict' will be removed in a future version");
1378   if(c->allow.n)
1379     error(0, "'allow' will be removed in a future version");
1380   if(c->trust.n)
1381     error(0, "'trust' will be removed in a future version");
1382   config = c;
1383   return 0;
1384 }
1385
1386 /** @brief Return the path to the private configuration file */
1387 char *config_private(void) {
1388   char *s;
1389
1390   set_configfile();
1391   byte_xasprintf(&s, "%s.private", configfile);
1392   return s;
1393 }
1394
1395 /** @brief Return the path to user's personal configuration file */
1396 char *config_userconf(const char *home, const struct passwd *pw) {
1397   char *s;
1398
1399   if(!home && !pw && !(pw = getpwuid(getuid())))
1400     fatal(0, "cannot determine our username");
1401   byte_xasprintf(&s, "%s/.disorder/passwd", home ? home : pw->pw_dir);
1402   return s;
1403 }
1404
1405 /** @brief Return the path to user-specific system configuration */
1406 char *config_usersysconf(const struct passwd *pw) {
1407   char *s;
1408
1409   set_configfile();
1410   if(!strchr(pw->pw_name, '/')) {
1411     byte_xasprintf(&s, "%s.%s", configfile, pw->pw_name);
1412     return s;
1413   } else
1414     return 0;
1415 }
1416
1417 char *config_get_file(const char *name) {
1418   return config_get_file2(config, name);
1419 }
1420
1421 /*
1422 Local Variables:
1423 c-basic-offset:2
1424 comment-column:40
1425 fill-column:79
1426 End:
1427 */