xref: /dragonfly/contrib/cvs-1.12/src/parseinfo.c (revision c89a6c1b)
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
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
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 *
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 = true;
307 #endif /* AUTH_SERVER_SUPPORT */
308 
309     return new;
310 }
311 
312 
313 
314 void
315 free_config (struct config *data)
316 {
317     if (data->keywords) free_keywords (data->keywords);
318     free (data);
319 }
320 
321 
322 
323 /* Return true if this function has already been called for line LN of file
324  * INFOPATH.
325  */
326 bool
327 parse_error (const char *infopath, unsigned int ln)
328 {
329     static List *errors = NULL;
330     char *nodename = NULL;
331 
332     if (!errors)
333 	errors = getlist();
334 
335     nodename = Xasprintf ("%s/%u", infopath, ln);
336     if (findnode (errors, nodename))
337     {
338 	free (nodename);
339 	return true;
340     }
341 
342     push_string (errors, nodename);
343     return false;
344 }
345 
346 
347 
348 #ifdef ALLOW_CONFIG_OVERRIDE
349 const char * const allowed_config_prefixes[] = { ALLOW_CONFIG_OVERRIDE };
350 #endif /* ALLOW_CONFIG_OVERRIDE */
351 
352 
353 
354 /* Parse the CVS config file.  The syntax right now is a bit ad hoc
355  * but tries to draw on the best or more common features of the other
356  * *info files and various unix (or non-unix) config file syntaxes.
357  * Lines starting with # are comments.  Settings are lines of the form
358  * KEYWORD=VALUE.  There is currently no way to have a multi-line
359  * VALUE (would be nice if there was, probably).
360  *
361  * CVSROOT is the $CVSROOT directory
362  * (current_parsed_root->directory might not be set yet, so this
363  * function takes the cvsroot as a function argument).
364  *
365  * RETURNS
366  *   Always returns a fully initialized config struct, which on error may
367  *   contain only the defaults.
368  *
369  * ERRORS
370  *   Calls error(0, ...) on errors in addition to the return value.
371  *
372  *   xmalloc() failures are fatal, per usual.
373  */
374 struct config *
375 parse_config (const char *cvsroot, const char *path)
376 {
377     const char *infopath;
378     char *freeinfopath = NULL;
379     FILE *fp_info;
380     char *line = NULL;
381     unsigned int ln;		/* Input file line counter.  */
382     char *buf = NULL;
383     size_t buf_allocated = 0;
384     size_t len;
385     char *p;
386     struct config *retval;
387     int rescan = 0;
388     /* PROCESSING	Whether config keys are currently being processed for
389      *			this root.
390      * PROCESSED	Whether any keys have been processed for this root.
391      *			This is initialized to true so that any initial keys
392      *			may be processed as global defaults.
393      */
394     bool processing = true;
395     bool processed = true;
396 
397     TRACE (TRACE_FUNCTION, "parse_config (%s)", cvsroot);
398 
399 #ifdef ALLOW_CONFIG_OVERRIDE
400     if (path)
401     {
402 	const char * const *prefix;
403 	char *npath = xcanonicalize_file_name (path);
404 	bool approved = false;
405 	for (prefix = allowed_config_prefixes; *prefix != NULL; prefix++)
406 	{
407 	    char *nprefix;
408 
409 	    if (!isreadable (*prefix)) continue;
410 	    nprefix = xcanonicalize_file_name (*prefix);
411 	    if (!strncmp (nprefix, npath, strlen (nprefix))
412 		&& (((*prefix)[strlen (*prefix)] != '/'
413 		     && strlen (npath) == strlen (nprefix))
414 		    || ((*prefix)[strlen (*prefix)] == '/'
415 			&& npath[strlen (nprefix)] == '/')))
416 		approved = true;
417 	    free (nprefix);
418 	    if (approved) break;
419 	}
420 	if (!approved)
421 	    error (1, 0, "Invalid path to config file specified: `%s'",
422 		   path);
423 	infopath = path;
424 	free (npath);
425     }
426     else
427 #endif
428 	infopath = freeinfopath =
429 	    Xasprintf ("%s/%s/%s", cvsroot, CVSROOTADM, CVSROOTADM_CONFIG);
430 
431     retval = new_config ();
432 
433 again:
434     fp_info = CVS_FOPEN (infopath, "r");
435     if (!fp_info)
436     {
437 	/* If no file, don't do anything special.  */
438 	if (!existence_error (errno))
439 	{
440 	    /* Just a warning message; doesn't affect return
441 	       value, currently at least.  */
442 	    error (0, errno, "cannot open %s", infopath);
443 	}
444 	if (freeinfopath) free (freeinfopath);
445 	return retval;
446     }
447 
448     ln = 0;  /* Have not read any lines yet.  */
449     while (getline (&buf, &buf_allocated, fp_info) >= 0)
450     {
451 	ln++; /* Keep track of input file line number for error messages.  */
452 
453 	line = buf;
454 
455 	/* Skip leading white space.  */
456 	while (isspace (*line)) line++;
457 
458 	/* Skip comments.  */
459 	if (line[0] == '#')
460 	    continue;
461 
462 	/* Is there any kind of written standard for the syntax of this
463 	   sort of config file?  Anywhere in POSIX for example (I guess
464 	   makefiles are sort of close)?  Red Hat Linux has a bunch of
465 	   these too (with some GUI tools which edit them)...
466 
467 	   Along the same lines, we might want a table of keywords,
468 	   with various types (boolean, string, &c), as a mechanism
469 	   for making sure the syntax is consistent.  Any good examples
470 	   to follow there (Apache?)?  */
471 
472 	/* Strip the trailing newline.  There will be one unless we
473 	   read a partial line without a newline, and then got end of
474 	   file (or error?).  */
475 
476 	len = strlen (line) - 1;
477 	if (line[len] == '\n')
478 	    line[len--] = '\0';
479 
480 	/* Skip blank lines.  */
481 	if (line[0] == '\0')
482 	    continue;
483 
484 	TRACE (TRACE_DATA, "parse_info() examining line: `%s'", line);
485 
486 	/* Check for a root specification.  */
487 	if (line[0] == '[' && line[len] == ']')
488 	{
489 	    cvsroot_t *tmproot;
490 
491 	    line++[len] = '\0';
492 	    tmproot = parse_cvsroot (line);
493 
494 	    /* Ignoring method.  */
495 	    if (!tmproot
496 #if defined CLIENT_SUPPORT || defined SERVER_SUPPORT
497 		|| (tmproot->method != local_method
498 		    && (!tmproot->hostname || !isThisHost (tmproot->hostname)))
499 #endif /* CLIENT_SUPPORT || SERVER_SUPPORT */
500 		|| !isSamePath (tmproot->directory, cvsroot))
501 	    {
502 		if (processed) processing = false;
503 	    }
504 	    else
505 	    {
506 		TRACE (TRACE_FLOW, "Matched root section`%s'", line);
507 		processing = true;
508 		processed = false;
509 	    }
510 
511 	    continue;
512 	}
513 
514 	/* There is data on this line.  */
515 
516 	/* Even if the data is bad or ignored, consider data processed for
517 	 * this root.
518 	 */
519 	processed = true;
520 
521 	if (!processing)
522 	    /* ...but it is for a different root.  */
523 	     continue;
524 
525 	/* The first '=' separates keyword from value.  */
526 	p = strchr (line, '=');
527 	if (!p)
528 	{
529 	    if (!parse_error (infopath, ln))
530 		error (0, 0,
531 "%s [%d]: syntax error: missing `=' between keyword and value",
532 		       infopath, ln);
533 	    continue;
534 	}
535 
536 	*p++ = '\0';
537 
538 	if (strcmp (line, "RCSBIN") == 0)
539 	{
540 	    /* This option used to specify the directory for RCS
541 	       executables.  But since we don't run them any more,
542 	       this is a noop.  Silently ignore it so that a
543 	       repository can work with either new or old CVS.  */
544 	    ;
545 	}
546 	else if (strcmp (line, "SystemAuth") == 0)
547 #ifdef AUTH_SERVER_SUPPORT
548 	    readBool (infopath, "SystemAuth", p, &retval->system_auth);
549 #else
550 	{
551 	    /* Still parse the syntax but ignore the option.  That way the same
552 	     * config file can be used for local and server.
553 	     */
554 	    bool dummy;
555 	    readBool (infopath, "SystemAuth", p, &dummy);
556 	}
557 #endif
558 	else if (strcmp (line, "LocalKeyword") == 0 ||
559 		 strcmp (line, "tag") == 0)
560 	    RCS_setlocalid (infopath, ln, &retval->keywords, p);
561 	else if (strcmp (line, "KeywordExpand") == 0 ||
562 		 strcmp (line, "tagexpand") == 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 	else if (strcmp (line, "RereadLogAfterVerify") == 0)
634 	{
635 	    if (!strcasecmp (p, "never"))
636 	      retval->RereadLogAfterVerify = LOGMSG_REREAD_NEVER;
637 	    else if (!strcasecmp (p, "always"))
638 	      retval->RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
639 	    else if (!strcasecmp (p, "stat"))
640 	      retval->RereadLogAfterVerify = LOGMSG_REREAD_STAT;
641 	    else
642 	    {
643 		bool tmp;
644 		if (readBool (infopath, "RereadLogAfterVerify", p, &tmp))
645 		{
646 		    if (tmp)
647 			retval->RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
648 		    else
649 			retval->RereadLogAfterVerify = LOGMSG_REREAD_NEVER;
650 		}
651 	    }
652 	}
653 	else if (strcmp (line, "TmpDir") == 0)
654 	{
655 	    if (retval->TmpDir) free (retval->TmpDir);
656 	    retval->TmpDir = expand_path (p, cvsroot, false, infopath, ln);
657 	    /* Could try some validity checking, like whether we can
658 	     * opendir it or something, but I don't see any particular
659 	     * reason to do that now rather than when the first function
660 	     * tries to create a temp file.
661 	     */
662 	}
663 	else if (strcmp (line, "UserAdminOptions") == 0)
664 	    retval->UserAdminOptions = xstrdup (p);
665 	else if (strcmp (line, "UseNewInfoFmtStrings") == 0)
666 #ifdef SUPPORT_OLD_INFO_FMT_STRINGS
667 	    readBool (infopath, "UseNewInfoFmtStrings", p,
668 		      &retval->UseNewInfoFmtStrings);
669 #else /* !SUPPORT_OLD_INFO_FMT_STRINGS */
670 	{
671 	    bool dummy;
672 	    if (readBool (infopath, "UseNewInfoFmtStrings", p, &dummy)
673 		&& !dummy)
674 		error (1, 0,
675 "%s [%u]: Old style info format strings not supported by this executable.",
676 		       infopath, ln);
677 	}
678 #endif /* SUPPORT_OLD_INFO_FMT_STRINGS */
679 	else if (strcmp (line, "ImportNewFilesToVendorBranchOnly") == 0)
680 	    readBool (infopath, "ImportNewFilesToVendorBranchOnly", p,
681 		      &retval->ImportNewFilesToVendorBranchOnly);
682 	else if (strcmp (line, "PrimaryServer") == 0)
683 	    retval->PrimaryServer = parse_cvsroot (p);
684 #ifdef PROXY_SUPPORT
685 	else if (!strcmp (line, "MaxProxyBufferSize"))
686 	    readSizeT (infopath, "MaxProxyBufferSize", p,
687 		       &retval->MaxProxyBufferSize);
688 #endif /* PROXY_SUPPORT */
689 	else if (!strcmp (line, "MaxCommentLeaderLength"))
690 	    readSizeT (infopath, "MaxCommentLeaderLength", p,
691 		       &retval->MaxCommentLeaderLength);
692 	else if (!strcmp (line, "UseArchiveCommentLeader"))
693 	    readBool (infopath, "UseArchiveCommentLeader", p,
694 		      &retval->UseArchiveCommentLeader);
695 #ifdef SERVER_SUPPORT
696 	else if (!strcmp (line, "MinCompressionLevel"))
697 	    readSizeT (infopath, "MinCompressionLevel", p,
698 		       &retval->MinCompressionLevel);
699 	else if (!strcmp (line, "MaxCompressionLevel"))
700 	    readSizeT (infopath, "MaxCompressionLevel", p,
701 		       &retval->MaxCompressionLevel);
702 #endif /* SERVER_SUPPORT */
703 	else
704 	    /* We may be dealing with a keyword which was added in a
705 	       subsequent version of CVS.  In that case it is a good idea
706 	       to complain, as (1) the keyword might enable a behavior like
707 	       alternate locking behavior, in which it is dangerous and hard
708 	       to detect if some CVS's have it one way and others have it
709 	       the other way, (2) in general, having us not do what the user
710 	       had in mind when they put in the keyword violates the
711 	       principle of least surprise.  Note that one corollary is
712 	       adding new keywords to your CVSROOT/config file is not
713 	       particularly recommended unless you are planning on using
714 	       the new features.  */
715 	    if (!parse_error (infopath, ln))
716 		error (0, 0, "%s [%u]: unrecognized keyword `%s'",
717 		       infopath, ln, line);
718     }
719     if (ferror (fp_info))
720 	error (0, errno, "cannot read %s", infopath);
721     if (fclose (fp_info) < 0)
722 	error (0, errno, "cannot close %s", infopath);
723     if (freeinfopath) free (freeinfopath);
724     if (buf) free (buf);
725 
726     if (path == NULL && !rescan++) {
727 	infopath = freeinfopath =
728 	    Xasprintf ("%s/%s/%s", cvsroot, CVSROOTADM, CVSROOTADM_OPTIONS);
729 	buf = NULL;
730 	goto again;
731     }
732 
733     return retval;
734 }
735