1/*
2 * Copyright (C) 1986-2005 The Free Software Foundation, Inc.
3 *
4 * Portions Copyright (C) 1998-2005 Derek Price, Ximbiot <http://ximbiot.com>,
5 *                                  and others.
6 *
7 * Portions Copyright (C) 1992, Brian Berliner and Jeff Polk
8 * Portions Copyright (C) 1989-1992, Brian Berliner
9 *
10 * You may distribute under the terms of the GNU General Public License as
11 * specified in the README file that comes with the CVS source distribution.
12 */
13#include <sys/cdefs.h>
14__RCSID("$NetBSD: parseinfo.c,v 1.4 2016/05/17 14:00:09 christos Exp $");
15
16#include "cvs.h"
17#include "getline.h"
18#include "history.h"
19
20/*
21 * Parse the INFOFILE file for the specified REPOSITORY.  Invoke CALLPROC for
22 * the first line in the file that matches the REPOSITORY, or if ALL != 0, any
23 * lines matching "ALL", or if no lines match, the last line matching
24 * "DEFAULT".
25 *
26 * Return 0 for success, -1 if there was not an INFOFILE, and >0 for failure.
27 */
28int
29Parse_Info (const char *infofile, const char *repository, CALLPROC callproc,
30            int opt, void *closure)
31{
32    int err = 0;
33    FILE *fp_info;
34    char *infopath;
35    char *line = NULL;
36    size_t line_allocated = 0;
37    char *default_value = NULL;
38    int default_line = 0;
39    char *expanded_value;
40    bool callback_done;
41    int line_number;
42    char *cp, *exp, *value;
43    const char *srepos;
44    const char *regex_err;
45
46    assert (repository);
47
48    if (!current_parsed_root)
49    {
50	/* XXX - should be error maybe? */
51	error (0, 0, "CVSROOT variable not set");
52	return 1;
53    }
54
55    /* find the info file and open it */
56    infopath = Xasprintf ("%s/%s/%s", current_parsed_root->directory,
57			  CVSROOTADM, infofile);
58    fp_info = CVS_FOPEN (infopath, "r");
59    if (!fp_info)
60    {
61	/* If no file, don't do anything special.  */
62	if (!existence_error (errno))
63	    error (0, errno, "cannot open %s", infopath);
64	free (infopath);
65	return 0;
66    }
67
68    /* strip off the CVSROOT if repository was absolute */
69    srepos = Short_Repository (repository);
70
71    TRACE (TRACE_FUNCTION, "Parse_Info (%s, %s, %s)",
72	   infopath, srepos,  (opt & PIOPT_ALL) ? "ALL" : "not ALL");
73
74    /* search the info file for lines that match */
75    callback_done = false;
76    line_number = 0;
77    while (getline (&line, &line_allocated, fp_info) >= 0)
78    {
79	line_number++;
80
81	/* skip lines starting with # */
82	if (line[0] == '#')
83	    continue;
84
85	/* skip whitespace at beginning of line */
86	for (cp = line; *cp && isspace ((unsigned char) *cp); cp++)
87	    ;
88
89	/* if *cp is null, the whole line was blank */
90	if (*cp == '\0')
91	    continue;
92
93	/* the regular expression is everything up to the first space */
94	for (exp = cp; *cp && !isspace ((unsigned char) *cp); cp++)
95	    ;
96	if (*cp != '\0')
97	    *cp++ = '\0';
98
99	/* skip whitespace up to the start of the matching value */
100	while (*cp && isspace ((unsigned char) *cp))
101	    cp++;
102
103	/* no value to match with the regular expression is an error */
104	if (*cp == '\0')
105	{
106	    error (0, 0, "syntax error at line %d file %s; ignored",
107		   line_number, infopath);
108	    continue;
109	}
110	value = cp;
111
112	/* strip the newline off the end of the value */
113	cp = strrchr (value, '\n');
114	if (cp) *cp = '\0';
115
116	/*
117	 * At this point, exp points to the regular expression, and value
118	 * points to the value to call the callback routine with.  Evaluate
119	 * the regular expression against srepos and callback with the value
120	 * if it matches.
121	 */
122
123	/* save the default value so we have it later if we need it */
124	if (strcmp (exp, "DEFAULT") == 0)
125	{
126	    if (default_value)
127	    {
128		error (0, 0, "Multiple `DEFAULT' lines (%d and %d) in %s file",
129		       default_line, line_number, infofile);
130		free (default_value);
131	    }
132	    default_value = xstrdup (value);
133	    default_line = line_number;
134	    continue;
135	}
136
137	/*
138	 * For a regular expression of "ALL", do the callback always We may
139	 * execute lots of ALL callbacks in addition to *one* regular matching
140	 * callback or default
141	 */
142	if (strcmp (exp, "ALL") == 0)
143	{
144	    if (!(opt & PIOPT_ALL))
145		error (0, 0, "Keyword `ALL' is ignored at line %d in %s file",
146		       line_number, infofile);
147	    else if ((expanded_value =
148			expand_path (value, current_parsed_root->directory,
149				     true, infofile, line_number)))
150	    {
151		err += callproc (repository, expanded_value, closure);
152		free (expanded_value);
153	    }
154	    else
155		err++;
156	    continue;
157	}
158
159	if (callback_done)
160	    /* only first matching, plus "ALL"'s */
161	    continue;
162
163	/* see if the repository matched this regular expression */
164	regex_err = re_comp (exp);
165	if (regex_err)
166	{
167	    error (0, 0, "bad regular expression at line %d file %s: %s",
168		   line_number, infofile, regex_err);
169	    continue;
170	}
171	if (re_exec (srepos) == 0)
172	    continue;				/* no match */
173
174	/* it did, so do the callback and note that we did one */
175	expanded_value = expand_path (value, current_parsed_root->directory,
176				      true, infofile, line_number);
177	if (expanded_value)
178	{
179	    err += callproc (repository, expanded_value, closure);
180	    free (expanded_value);
181	}
182	else
183	    err++;
184	callback_done = true;
185    }
186    if (ferror (fp_info))
187	error (0, errno, "cannot read %s", infopath);
188    if (fclose (fp_info) < 0)
189	error (0, errno, "cannot close %s", infopath);
190
191    /* if we fell through and didn't callback at all, do the default */
192    if (!callback_done && default_value)
193    {
194	expanded_value = expand_path (default_value,
195				      current_parsed_root->directory,
196				      true, infofile, line_number);
197	if (expanded_value)
198	{
199	    err += callproc (repository, expanded_value, closure);
200	    free (expanded_value);
201	}
202	else
203	    err++;
204    }
205
206    /* free up space if necessary */
207    if (default_value) free (default_value);
208    free (infopath);
209    if (line) free (line);
210
211    return err;
212}
213
214
215
216/* Print a warning and return false if P doesn't look like a string specifying
217 * something that can be converted into a size_t.
218 *
219 * Sets *VAL to the parsed value when it is found to be valid.  *VAL will not
220 * be altered when false is returned.
221 */
222static bool
223readSizeT (const char *infopath, const char *option, const char *p,
224	   size_t *val)
225{
226    const char *q;
227    size_t num, factor = 1;
228
229    if (!strcasecmp ("unlimited", p))
230    {
231	*val = SIZE_MAX;
232	return true;
233    }
234
235    /* Record the factor character (kilo, mega, giga, tera).  */
236    if (!isdigit (p[strlen(p) - 1]))
237    {
238	switch (p[strlen(p) - 1])
239	{
240	    case 'T':
241		factor = xtimes (factor, 1024);
242	    case 'G':
243		factor = xtimes (factor, 1024);
244	    case 'M':
245		factor = xtimes (factor, 1024);
246	    case 'k':
247		factor = xtimes (factor, 1024);
248		break;
249	    default:
250		error (0, 0,
251    "%s: Unknown %s factor: `%c'",
252		       infopath, option, p[strlen(p)]);
253		return false;
254	}
255	TRACE (TRACE_DATA, "readSizeT(): Found factor %zu for %s",
256	       factor, option);
257    }
258
259    /* Verify that *q is a number.  */
260    q = p;
261    while (q < p + strlen(p) - 1 /* Checked last character above.  */)
262    {
263	if (!isdigit(*q))
264	{
265	    error (0, 0,
266"%s: %s must be a postitive integer, not '%s'",
267		   infopath, option, p);
268	    return false;
269	}
270	q++;
271    }
272
273    /* Compute final value.  */
274    num = strtoul (p, NULL, 10);
275    if (num == ULONG_MAX || num > SIZE_MAX)
276	/* Don't return an error, just max out.  */
277	num = SIZE_MAX;
278
279    TRACE (TRACE_DATA, "readSizeT(): read number %zu for %s", num, option);
280    *val = xtimes (strtoul (p, NULL, 10), factor);
281    TRACE (TRACE_DATA, "readSizeT(): returnning %zu for %s", *val, option);
282    return true;
283}
284
285
286
287/* Allocate and initialize a new config struct.  */
288static inline struct config *
289new_config (void)
290{
291    struct config *new = xcalloc (1, sizeof (struct config));
292
293    TRACE (TRACE_FLOW, "new_config ()");
294
295    new->logHistory = xstrdup (ALL_HISTORY_REC_TYPES);
296    new->RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
297    new->UserAdminOptions = xstrdup ("k");
298#ifdef CVS_ADMIN_GROUP
299    new->UserAdminGroup = xstrdup (CVS_ADMIN_GROUP);
300#else
301    new->UserAdminGroup = NULL;
302#endif
303    new->MaxCommentLeaderLength = 20;
304#ifdef SERVER_SUPPORT
305    new->MaxCompressionLevel = 9;
306#endif /* SERVER_SUPPORT */
307#ifdef PROXY_SUPPORT
308    new->MaxProxyBufferSize = (size_t)(8 * 1024 * 1024); /* 8 megabytes,
309                                                          * by default.
310                                                          */
311#endif /* PROXY_SUPPORT */
312#ifdef AUTH_SERVER_SUPPORT
313    new->system_auth = true;
314#endif /* AUTH_SERVER_SUPPORT */
315
316    return new;
317}
318
319
320
321void
322free_config (struct config *data)
323{
324    if (data->keywords) free_keywords (data->keywords);
325    free (data);
326}
327
328
329
330/* Return true if this function has already been called for line LN of file
331 * INFOPATH.
332 */
333bool
334parse_error (const char *infopath, unsigned int ln)
335{
336    static List *errors = NULL;
337    char *nodename = NULL;
338
339    if (!errors)
340	errors = getlist();
341
342    nodename = Xasprintf ("%s/%u", infopath, ln);
343    if (findnode (errors, nodename))
344    {
345	free (nodename);
346	return true;
347    }
348
349    push_string (errors, nodename);
350    return false;
351}
352
353
354
355#ifdef ALLOW_CONFIG_OVERRIDE
356const char * const allowed_config_prefixes[] = { ALLOW_CONFIG_OVERRIDE };
357#endif /* ALLOW_CONFIG_OVERRIDE */
358
359
360
361/* Parse the CVS config file.  The syntax right now is a bit ad hoc
362 * but tries to draw on the best or more common features of the other
363 * *info files and various unix (or non-unix) config file syntaxes.
364 * Lines starting with # are comments.  Settings are lines of the form
365 * KEYWORD=VALUE.  There is currently no way to have a multi-line
366 * VALUE (would be nice if there was, probably).
367 *
368 * CVSROOT is the $CVSROOT directory
369 * (current_parsed_root->directory might not be set yet, so this
370 * function takes the cvsroot as a function argument).
371 *
372 * RETURNS
373 *   Always returns a fully initialized config struct, which on error may
374 *   contain only the defaults.
375 *
376 * ERRORS
377 *   Calls error(0, ...) on errors in addition to the return value.
378 *
379 *   xmalloc() failures are fatal, per usual.
380 */
381struct config *
382parse_config (const char *cvsroot, const char *path)
383{
384    const char *infopath;
385    char *freeinfopath = NULL;
386    FILE *fp_info;
387    char *line = NULL;
388    unsigned int ln;		/* Input file line counter.  */
389    char *buf = NULL;
390    size_t buf_allocated = 0;
391    size_t len;
392    char *p;
393    struct config *retval;
394    /* PROCESSING	Whether config keys are currently being processed for
395     *			this root.
396     * PROCESSED	Whether any keys have been processed for this root.
397     *			This is initialized to true so that any initial keys
398     *			may be processed as global defaults.
399     */
400    bool processing = true;
401    bool processed = true;
402
403    TRACE (TRACE_FUNCTION, "parse_config (%s)", cvsroot);
404
405#ifdef ALLOW_CONFIG_OVERRIDE
406    if (path)
407    {
408	const char * const *prefix;
409	char *npath = xcanonicalize_file_name (path);
410	bool approved = false;
411	for (prefix = allowed_config_prefixes; *prefix != NULL; prefix++)
412	{
413	    char *nprefix;
414
415	    if (!isreadable (*prefix)) continue;
416	    nprefix = xcanonicalize_file_name (*prefix);
417	    if (!strncmp (nprefix, npath, strlen (nprefix))
418		&& (((*prefix)[strlen (*prefix)] != '/'
419		     && strlen (npath) == strlen (nprefix))
420		    || ((*prefix)[strlen (*prefix)] == '/'
421			&& npath[strlen (nprefix)] == '/')))
422		approved = true;
423	    free (nprefix);
424	    if (approved) break;
425	}
426	if (!approved)
427	    error (1, 0, "Invalid path to config file specified: `%s'",
428		   path);
429	infopath = path;
430	free (npath);
431    }
432    else
433#endif
434	infopath = freeinfopath =
435	    Xasprintf ("%s/%s/%s", cvsroot, CVSROOTADM, CVSROOTADM_CONFIG);
436
437    retval = new_config ();
438
439    fp_info = CVS_FOPEN (infopath, "r");
440    if (!fp_info)
441    {
442	/* If no file, don't do anything special.  */
443	if (!existence_error (errno))
444	{
445	    /* Just a warning message; doesn't affect return
446	       value, currently at least.  */
447	    error (0, errno, "cannot open %s", infopath);
448	}
449	if (freeinfopath) free (freeinfopath);
450	return retval;
451    }
452
453    ln = 0;  /* Have not read any lines yet.  */
454    while (getline (&buf, &buf_allocated, fp_info) >= 0)
455    {
456	ln++; /* Keep track of input file line number for error messages.  */
457
458	line = buf;
459
460	/* Skip leading white space.  */
461	while (isspace (*line)) line++;
462
463	/* Skip comments.  */
464	if (line[0] == '#')
465	    continue;
466
467	/* Is there any kind of written standard for the syntax of this
468	   sort of config file?  Anywhere in POSIX for example (I guess
469	   makefiles are sort of close)?  Red Hat Linux has a bunch of
470	   these too (with some GUI tools which edit them)...
471
472	   Along the same lines, we might want a table of keywords,
473	   with various types (boolean, string, &c), as a mechanism
474	   for making sure the syntax is consistent.  Any good examples
475	   to follow there (Apache?)?  */
476
477	/* Strip the trailing newline.  There will be one unless we
478	   read a partial line without a newline, and then got end of
479	   file (or error?).  */
480
481	len = strlen (line) - 1;
482	if (line[len] == '\n')
483	    line[len--] = '\0';
484
485	/* Skip blank lines.  */
486	if (line[0] == '\0')
487	    continue;
488
489	TRACE (TRACE_DATA, "parse_info() examining line: `%s'", line);
490
491	/* Check for a root specification.  */
492	if (line[0] == '[' && line[len] == ']')
493	{
494	    cvsroot_t *tmproot;
495
496	    line++[len] = '\0';
497	    tmproot = parse_cvsroot (line);
498
499	    /* Ignoring method.  */
500	    if (!tmproot
501#if defined CLIENT_SUPPORT || defined SERVER_SUPPORT
502		|| (tmproot->method != local_method
503		    && (!tmproot->hostname || !isThisHost (tmproot->hostname)))
504#endif /* CLIENT_SUPPORT || SERVER_SUPPORT */
505		|| !isSamePath (tmproot->directory, cvsroot))
506	    {
507		if (processed) processing = false;
508	    }
509	    else
510	    {
511		TRACE (TRACE_FLOW, "Matched root section`%s'", line);
512		processing = true;
513		processed = false;
514	    }
515
516	    continue;
517	}
518
519	/* There is data on this line.  */
520
521	/* Even if the data is bad or ignored, consider data processed for
522	 * this root.
523	 */
524	processed = true;
525
526	if (!processing)
527	    /* ...but it is for a different root.  */
528	     continue;
529
530	/* The first '=' separates keyword from value.  */
531	p = strchr (line, '=');
532	if (!p)
533	{
534	    if (!parse_error (infopath, ln))
535		error (0, 0,
536"%s [%d]: syntax error: missing `=' between keyword and value",
537		       infopath, ln);
538	    continue;
539	}
540
541	*p++ = '\0';
542
543	if (strcmp (line, "RCSBIN") == 0)
544	{
545	    /* This option used to specify the directory for RCS
546	       executables.  But since we don't run them any more,
547	       this is a noop.  Silently ignore it so that a
548	       repository can work with either new or old CVS.  */
549	    ;
550	}
551	else if (strcmp (line, "SystemAuth") == 0)
552#ifdef AUTH_SERVER_SUPPORT
553	    readBool (infopath, "SystemAuth", p, &retval->system_auth);
554#else
555	{
556	    /* Still parse the syntax but ignore the option.  That way the same
557	     * config file can be used for local and server.
558	     */
559	    bool dummy;
560	    readBool (infopath, "SystemAuth", p, &dummy);
561	}
562#endif
563	else if (strcmp (line, "LocalKeyword") == 0 ||
564	    strcmp (line, "tag") == 0)
565	    RCS_setlocalid (infopath, ln, &retval->keywords, p);
566	else if (strcmp (line, "KeywordExpand") == 0)
567	    RCS_setincexc (&retval->keywords, p);
568	else if (strcmp (line, "PreservePermissions") == 0)
569	{
570#ifdef PRESERVE_PERMISSIONS_SUPPORT
571	    readBool (infopath, "PreservePermissions", p,
572		      &retval->preserve_perms);
573#else
574	    if (!parse_error (infopath, ln))
575		error (0, 0, "\
576%s [%u]: warning: this CVS does not support PreservePermissions",
577		       infopath, ln);
578#endif
579	}
580	else if (strcmp (line, "TopLevelAdmin") == 0)
581	    readBool (infopath, "TopLevelAdmin", p, &retval->top_level_admin);
582	else if (strcmp (line, "LockDir") == 0)
583	{
584	    if (retval->lock_dir)
585		free (retval->lock_dir);
586	    retval->lock_dir = expand_path (p, cvsroot, false, infopath, ln);
587	    /* Could try some validity checking, like whether we can
588	       opendir it or something, but I don't see any particular
589	       reason to do that now rather than waiting until lock.c.  */
590	}
591	else if (strcmp (line, "HistoryLogPath") == 0)
592	{
593	    if (retval->HistoryLogPath) free (retval->HistoryLogPath);
594
595	    /* Expand ~ & $VARs.  */
596	    retval->HistoryLogPath = expand_path (p, cvsroot, false,
597						  infopath, ln);
598
599	    if (retval->HistoryLogPath && !ISABSOLUTE (retval->HistoryLogPath))
600	    {
601		error (0, 0, "%s [%u]: HistoryLogPath must be absolute.",
602		       infopath, ln);
603		free (retval->HistoryLogPath);
604		retval->HistoryLogPath = NULL;
605	    }
606	}
607	else if (strcmp (line, "HistorySearchPath") == 0)
608	{
609	    if (retval->HistorySearchPath) free (retval->HistorySearchPath);
610	    retval->HistorySearchPath = expand_path (p, cvsroot, false,
611						     infopath, ln);
612
613	    if (retval->HistorySearchPath
614		&& !ISABSOLUTE (retval->HistorySearchPath))
615	    {
616		error (0, 0, "%s [%u]: HistorySearchPath must be absolute.",
617		       infopath, ln);
618		free (retval->HistorySearchPath);
619		retval->HistorySearchPath = NULL;
620	    }
621	}
622	else if (strcmp (line, "LogHistory") == 0)
623	{
624	    if (strcmp (p, "all") != 0)
625	    {
626		static bool gotone = false;
627		if (gotone)
628		    error (0, 0, "\
629%s [%u]: warning: duplicate LogHistory entry found.",
630			   infopath, ln);
631		else
632		    gotone = true;
633		free (retval->logHistory);
634		retval->logHistory = xstrdup (p);
635	    }
636	}
637	else if (strcmp (line, "RereadLogAfterVerify") == 0)
638	{
639	    if (!strcasecmp (p, "never"))
640	      retval->RereadLogAfterVerify = LOGMSG_REREAD_NEVER;
641	    else if (!strcasecmp (p, "always"))
642	      retval->RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
643	    else if (!strcasecmp (p, "stat"))
644	      retval->RereadLogAfterVerify = LOGMSG_REREAD_STAT;
645	    else
646	    {
647		bool tmp;
648		if (readBool (infopath, "RereadLogAfterVerify", p, &tmp))
649		{
650		    if (tmp)
651			retval->RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
652		    else
653			retval->RereadLogAfterVerify = LOGMSG_REREAD_NEVER;
654		}
655	    }
656	}
657	else if (strcmp (line, "TmpDir") == 0)
658	{
659	    if (retval->TmpDir) free (retval->TmpDir);
660	    retval->TmpDir = expand_path (p, cvsroot, false, infopath, ln);
661	    /* Could try some validity checking, like whether we can
662	     * opendir it or something, but I don't see any particular
663	     * reason to do that now rather than when the first function
664	     * tries to create a temp file.
665	     */
666	}
667	else if (strcmp (line, "UserAdminGroup") == 0
668	    || strcmp (line, "AdminGroup") == 0)
669	    retval->UserAdminGroup = xstrdup (p);
670	else if (strcmp (line, "UserAdminOptions") == 0
671	    || strcmp (line, "AdminOptions") == 0)
672	    retval->UserAdminOptions = xstrdup (p);
673	else if (strcmp (line, "UseNewInfoFmtStrings") == 0)
674#ifdef SUPPORT_OLD_INFO_FMT_STRINGS
675	    readBool (infopath, "UseNewInfoFmtStrings", p,
676		      &retval->UseNewInfoFmtStrings);
677#else /* !SUPPORT_OLD_INFO_FMT_STRINGS */
678	{
679	    bool dummy;
680	    if (readBool (infopath, "UseNewInfoFmtStrings", p, &dummy)
681		&& !dummy)
682		error (1, 0,
683"%s [%u]: Old style info format strings not supported by this executable.",
684		       infopath, ln);
685	}
686#endif /* SUPPORT_OLD_INFO_FMT_STRINGS */
687	else if (strcmp (line, "ImportNewFilesToVendorBranchOnly") == 0)
688	    readBool (infopath, "ImportNewFilesToVendorBranchOnly", p,
689		      &retval->ImportNewFilesToVendorBranchOnly);
690	else if (strcmp (line, "PrimaryServer") == 0)
691	    retval->PrimaryServer = parse_cvsroot (p);
692#ifdef PROXY_SUPPORT
693	else if (!strcmp (line, "MaxProxyBufferSize"))
694	    readSizeT (infopath, "MaxProxyBufferSize", p,
695		       &retval->MaxProxyBufferSize);
696#endif /* PROXY_SUPPORT */
697	else if (!strcmp (line, "MaxCommentLeaderLength"))
698	    readSizeT (infopath, "MaxCommentLeaderLength", p,
699		       &retval->MaxCommentLeaderLength);
700	else if (!strcmp (line, "UseArchiveCommentLeader"))
701	    readBool (infopath, "UseArchiveCommentLeader", p,
702		      &retval->UseArchiveCommentLeader);
703#ifdef SERVER_SUPPORT
704	else if (!strcmp (line, "MinCompressionLevel"))
705	    readSizeT (infopath, "MinCompressionLevel", p,
706		       &retval->MinCompressionLevel);
707	else if (!strcmp (line, "MaxCompressionLevel"))
708	    readSizeT (infopath, "MaxCompressionLevel", p,
709		       &retval->MaxCompressionLevel);
710#endif /* SERVER_SUPPORT */
711	else
712	    /* We may be dealing with a keyword which was added in a
713	       subsequent version of CVS.  In that case it is a good idea
714	       to complain, as (1) the keyword might enable a behavior like
715	       alternate locking behavior, in which it is dangerous and hard
716	       to detect if some CVS's have it one way and others have it
717	       the other way, (2) in general, having us not do what the user
718	       had in mind when they put in the keyword violates the
719	       principle of least surprise.  Note that one corollary is
720	       adding new keywords to your CVSROOT/config file is not
721	       particularly recommended unless you are planning on using
722	       the new features.  */
723	    if (!parse_error (infopath, ln))
724		error (0, 0, "%s [%u]: unrecognized keyword `%s'",
725		       infopath, ln, line);
726    }
727    if (ferror (fp_info))
728	error (0, errno, "cannot read %s", infopath);
729    if (fclose (fp_info) < 0)
730	error (0, errno, "cannot close %s", infopath);
731    if (freeinfopath) free (freeinfopath);
732    if (buf) free (buf);
733
734    return retval;
735}
736
737/* cvsacl patch */
738int
739parse_aclconfig (const char *cvsroot)
740{
741    char *infopath;
742    FILE *fp_info;
743    char *line = NULL;
744    size_t line_allocated = 0;
745    size_t len;
746    char *p;
747    /* FIXME-reentrancy: If we do a multi-threaded server, this would need
748       to go to the per-connection data structures.  */
749    static int parsed = 0;
750
751    /* Authentication code and serve_root might both want to call us.
752       Let this happen smoothly.  */
753    if (parsed)
754	return 0;
755    parsed = 1;
756
757    infopath = xmalloc (strlen (cvsroot)
758			+ sizeof (CVSROOTADM_ACLCONFIG)
759			+ sizeof (CVSROOTADM)
760			+ 10);
761    if (infopath == NULL)
762    {
763	error (0, 0, "out of memory; cannot allocate infopath");
764	goto error_return;
765    }
766
767    strcpy (infopath, cvsroot);
768    strcat (infopath, "/");
769    strcat (infopath, CVSROOTADM);
770    strcat (infopath, "/");
771    strcat (infopath, CVSROOTADM_ACLCONFIG);
772
773    fp_info = CVS_FOPEN (infopath, "r");
774    if (fp_info == NULL)
775    {
776	/* If no file, don't do anything special.  */
777	if (!existence_error (errno))
778	{
779	    /* Just a warning message; doesn't affect return
780	       value, currently at least.  */
781	    error (0, errno, "cannot open %s", infopath);
782	}
783	free (infopath);
784	return 0;
785    }
786
787    while (getline (&line, &line_allocated, fp_info) >= 0)
788    {
789	/* Skip comments.  */
790	if (line[0] == '#')
791	    continue;
792
793	len = strlen (line) - 1;
794	if (line[len] == '\n')
795	    line[len] = '\0';
796
797	/* Skip blank lines.  */
798	if (line[0] == '\0')
799	    continue;
800
801	/* The first '=' separates keyword from value.  */
802	p = strchr (line, '=');
803	if (p == NULL)
804	{
805	    /* Probably should be printing line number.  */
806	    error (0, 0, "syntax error in %s: line '%s' is missing '='",
807		   infopath, line);
808	    goto error_return;
809	}
810
811	*p++ = '\0';
812
813	if (strcmp (line, "UseCVSACL") == 0)
814	{
815	    if (strcmp (p, "no") == 0)
816		use_cvs_acl = 0;
817	    else if (strcmp (p, "yes") == 0)
818		use_cvs_acl = 1;
819	    else
820	    {
821		error (0, 0, "unrecognized value '%s' for UseCVSACL", p);
822		goto error_return;
823	    }
824	}
825	else if (strcmp (line, "UseSeperateACLFileForEachDir") == 0)
826	{
827	    if (strcmp (p, "no") == 0)
828		use_separate_acl_file_for_each_dir = 0;
829	    else if (strcmp (p, "yes") == 0)
830		use_separate_acl_file_for_each_dir = 1;
831	    else
832	    {
833		error (0, 0, "unrecognized value '%s' for UseSeperateACLFileForEachDir", p);
834		goto error_return;
835	    }
836	}
837	else if (strcmp (line, "StopAtFirstPermissionDenied") == 0)
838	{
839	    if (strcmp (p, "no") == 0)
840		stop_at_first_permission_denied = 0;
841	    else if (strcmp (p, "yes") == 0)
842		stop_at_first_permission_denied = 1;
843	    else
844	    {
845		error (0, 0, "unrecognized value '%s' for StopAtFirstPermissionDenied", p);
846		goto error_return;
847	    }
848	}
849	else if (strcmp (line, "CVSACLDefaultPermissions") == 0)
850	{
851	    if (cvs_acl_default_permissions != NULL)
852		free (cvs_acl_default_permissions);
853			if (!given_perms_valid (p))
854				error (1,0,"Invalid CVS ACL Default Permissions: '%s' in CVSROOT/aclconfig", p);
855		cvs_acl_default_permissions = xstrdup (p);
856	}
857	else if (strcmp (line, "UseCVSGroups") == 0)
858	{
859	    if (strcmp (p, "no") == 0)
860		use_cvs_groups = 0;
861	    else if (strcmp (p, "yes") == 0)
862		use_cvs_groups = 1;
863	    else
864	    {
865		error (0, 0, "unrecognized value '%s' for UseCVSGroups", p);
866		goto error_return;
867	    }
868	}
869	else if (strcmp (line, "UseSystemGroups") == 0)
870	{
871	    if (strcmp (p, "no") == 0)
872		use_system_groups = 0;
873	    else if (strcmp (p, "yes") == 0)
874		use_system_groups = 1;
875	    else
876	    {
877		error (0, 0, "unrecognized value '%s' for UseSystemGroups", p);
878		goto error_return;
879	    }
880	}
881	else if (strcmp (line, "CVSACLFileLocation") == 0)
882	{
883	    if (cvs_acl_file_location != NULL)
884		free (cvs_acl_file_location);
885		cvs_acl_file_location = xstrdup (p);
886	}
887	else if (strcmp (line, "CVSGroupsFileLocation") == 0)
888	{
889	    if (cvs_groups_file_location != NULL)
890		free (cvs_groups_file_location);
891		cvs_groups_file_location = xstrdup (p);
892	}
893	else if (strcmp (line, "CVSServerRunAsUser") == 0)
894	{
895	    if (cvs_server_run_as != NULL)
896		free (cvs_server_run_as);
897		cvs_server_run_as = xstrdup (p);
898	}
899
900    }
901
902    if (ferror (fp_info))
903    {
904	error (0, errno, "cannot read %s", infopath);
905	goto error_return;
906    }
907    if (fclose (fp_info) < 0)
908    {
909	error (0, errno, "cannot close %s", infopath);
910	goto error_return;
911    }
912    free (infopath);
913    if (line != NULL)
914	free (line);
915    return 0;
916
917 error_return:
918    if (infopath != NULL)
919	free (infopath);
920    if (line != NULL)
921	free (line);
922    return -1;
923}
924