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