update changelog
[alioth/cvs.git] / src / parseinfo.c
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 (kibi, mebi, gibi, tebi).  */
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) - 1]);
251                 return false;
252         }
253         TRACE (TRACE_DATA, "readSizeT(): Found factor %zu 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 %zu for %s", num, option);
278     *val = xtimes (strtoul (p, NULL, 10), factor);
279     TRACE (TRACE_DATA, "readSizeT(): returnning %zu 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 mebibytes,
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     /* PROCESSING       Whether config keys are currently being processed for
388      *                  this root.
389      * PROCESSED        Whether any keys have been processed for this root.
390      *                  This is initialized to true so that any initial keys
391      *                  may be processed as global defaults.
392      */
393     bool processing = true;
394     bool processed = true;
395 #ifdef SERVER_SUPPORT
396     size_t dummy_sizet;
397 #endif
398
399     TRACE (TRACE_FUNCTION, "parse_config (%s)", cvsroot);
400
401 #ifdef ALLOW_CONFIG_OVERRIDE
402     if (path)
403     {
404         const char * const *prefix;
405         char *npath = xcanonicalize_file_name (path);
406         bool approved = false;
407         for (prefix = allowed_config_prefixes; *prefix != NULL; prefix++)
408         {
409             char *nprefix;
410
411             if (!isreadable (*prefix)) continue;
412             nprefix = xcanonicalize_file_name (*prefix);
413             if (!strncmp (nprefix, npath, strlen (nprefix))
414                 && (((*prefix)[strlen (*prefix)] != '/'
415                      && strlen (npath) == strlen (nprefix))
416                     || ((*prefix)[strlen (*prefix)] == '/'
417                         && npath[strlen (nprefix)] == '/')))
418                 approved = true;
419             free (nprefix);
420             if (approved) break;
421         }
422         if (!approved)
423             error (1, 0, "Invalid path to config file specified: `%s'",
424                    path);
425         infopath = path;
426         free (npath);
427     }
428     else
429 #endif
430         infopath = freeinfopath =
431             Xasprintf ("%s/%s/%s", cvsroot, CVSROOTADM, CVSROOTADM_CONFIG);
432
433     retval = new_config ();
434
435     fp_info = CVS_FOPEN (infopath, "r");
436     if (!fp_info)
437     {
438         /* If no file, don't do anything special.  */
439         if (!existence_error (errno))
440         {
441             /* Just a warning message; doesn't affect return
442                value, currently at least.  */
443             error (0, errno, "cannot open %s", infopath);
444         }
445         if (freeinfopath) free (freeinfopath);
446         return retval;
447     }
448
449     ln = 0;  /* Have not read any lines yet.  */
450     while (getline (&buf, &buf_allocated, fp_info) >= 0)
451     {
452         ln++; /* Keep track of input file line number for error messages.  */
453
454         line = buf;
455
456         /* Skip leading white space.  */
457         while (isspace (*line)) line++;
458
459         /* Skip comments.  */
460         if (line[0] == '#')
461             continue;
462
463         /* Is there any kind of written standard for the syntax of this
464            sort of config file?  Anywhere in POSIX for example (I guess
465            makefiles are sort of close)?  Red Hat Linux has a bunch of
466            these too (with some GUI tools which edit them)...
467
468            Along the same lines, we might want a table of keywords,
469            with various types (boolean, string, &c), as a mechanism
470            for making sure the syntax is consistent.  Any good examples
471            to follow there (Apache?)?  */
472
473         /* Strip the trailing newline.  There will be one unless we
474            read a partial line without a newline, and then got end of
475            file (or error?).  */
476
477         len = strlen (line) - 1;
478         if (line[len] == '\n')
479             line[len--] = '\0';
480
481         /* Skip blank lines.  */
482         if (line[0] == '\0')
483             continue;
484
485         TRACE (TRACE_DATA, "parse_info() examining line: `%s'", line);
486
487         /* Check for a root specification.  */
488         if (line[0] == '[' && line[len] == ']')
489         {
490             cvsroot_t *tmproot;
491
492             line++[len] = '\0';
493             tmproot = parse_cvsroot (line);
494
495             /* Ignoring method.  */
496             if (!tmproot
497 #if defined CLIENT_SUPPORT || defined SERVER_SUPPORT
498                 || (tmproot->method != local_method
499                     && (!tmproot->hostname || !isThisHost (tmproot->hostname)))
500 #endif /* CLIENT_SUPPORT || SERVER_SUPPORT */
501                 || !isSamePath (tmproot->directory, cvsroot))
502             {
503                 if (processed) processing = false;
504             }
505             else
506             {
507                 TRACE (TRACE_FLOW, "Matched root section`%s'", line);
508                 processing = true;
509                 processed = false;
510             }
511
512             continue;
513         }
514
515         /* There is data on this line.  */
516
517         /* Even if the data is bad or ignored, consider data processed for
518          * this root.
519          */
520         processed = true;
521
522         if (!processing)
523             /* ...but it is for a different root.  */
524              continue;
525
526         /* The first '=' separates keyword from value.  */
527         p = strchr (line, '=');
528         if (!p)
529         {
530             if (!parse_error (infopath, ln))
531                 error (0, 0,
532 "%s [%d]: syntax error: missing `=' between keyword and value",
533                        infopath, ln);
534             continue;
535         }
536
537         *p++ = '\0';
538
539         if (strcmp (line, "RCSBIN") == 0)
540         {
541             /* This option used to specify the directory for RCS
542                executables.  But since we don't run them any more,
543                this is a noop.  Silently ignore it so that a
544                repository can work with either new or old CVS.  */
545             ;
546         }
547         else if (strcmp (line, "SystemAuth") == 0)
548 #ifdef AUTH_SERVER_SUPPORT
549             readBool (infopath, "SystemAuth", p, &retval->system_auth);
550 #else
551         {
552             /* Still parse the syntax but ignore the option.  That way the same
553              * config file can be used for local and server.
554              */
555             bool dummy;
556             readBool (infopath, "SystemAuth", p, &dummy);
557         }
558 #endif
559         else if (strcmp (line, "LocalKeyword") == 0)
560             RCS_setlocalid (infopath, ln, &retval->keywords, p);
561         else if (strcmp (line, "KeywordExpand") == 0)
562             RCS_setincexc (&retval->keywords, p);
563         else if (strcmp (line, "PreservePermissions") == 0)
564         {
565 #ifdef PRESERVE_PERMISSIONS_SUPPORT
566             readBool (infopath, "PreservePermissions", p,
567                       &retval->preserve_perms);
568 #else
569             if (!parse_error (infopath, ln))
570                 error (0, 0, "\
571 %s [%u]: warning: this CVS does not support PreservePermissions",
572                        infopath, ln);
573 #endif
574         }
575         else if (strcmp (line, "TopLevelAdmin") == 0)
576             readBool (infopath, "TopLevelAdmin", p, &retval->top_level_admin);
577         else if (strcmp (line, "LockDir") == 0)
578         {
579             if (retval->lock_dir)
580                 free (retval->lock_dir);
581             retval->lock_dir = expand_path (p, cvsroot, false, infopath, ln);
582             /* Could try some validity checking, like whether we can
583                opendir it or something, but I don't see any particular
584                reason to do that now rather than waiting until lock.c.  */
585         }
586         else if (strcmp (line, "HistoryLogPath") == 0)
587         {
588             if (retval->HistoryLogPath) free (retval->HistoryLogPath);
589
590             /* Expand ~ & $VARs.  */
591             retval->HistoryLogPath = expand_path (p, cvsroot, false,
592                                                   infopath, ln);
593
594             if (retval->HistoryLogPath && !ISABSOLUTE (retval->HistoryLogPath))
595             {
596                 error (0, 0, "%s [%u]: HistoryLogPath must be absolute.",
597                        infopath, ln);
598                 free (retval->HistoryLogPath);
599                 retval->HistoryLogPath = NULL;
600             }
601         }
602         else if (strcmp (line, "HistorySearchPath") == 0)
603         {
604             if (retval->HistorySearchPath) free (retval->HistorySearchPath);
605             retval->HistorySearchPath = expand_path (p, cvsroot, false,
606                                                      infopath, ln);
607
608             if (retval->HistorySearchPath
609                 && !ISABSOLUTE (retval->HistorySearchPath))
610             {
611                 error (0, 0, "%s [%u]: HistorySearchPath must be absolute.",
612                        infopath, ln);
613                 free (retval->HistorySearchPath);
614                 retval->HistorySearchPath = NULL;
615             }
616         }
617         else if (strcmp (line, "LogHistory") == 0)
618         {
619             if (strcmp (p, "all") != 0)
620             {
621                 static bool gotone = false;
622                 if (gotone)
623                     error (0, 0, "\
624 %s [%u]: warning: duplicate LogHistory entry found.",
625                            infopath, ln);
626                 else
627                     gotone = true;
628                 free (retval->logHistory);
629                 retval->logHistory = xstrdup (p);
630             }
631         }
632         else if (strcmp (line, "RereadLogAfterVerify") == 0)
633         {
634             if (!strcasecmp (p, "never"))
635               retval->RereadLogAfterVerify = LOGMSG_REREAD_NEVER;
636             else if (!strcasecmp (p, "always"))
637               retval->RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
638             else if (!strcasecmp (p, "stat"))
639               retval->RereadLogAfterVerify = LOGMSG_REREAD_STAT;
640             else
641             {
642                 bool tmp;
643                 if (readBool (infopath, "RereadLogAfterVerify", p, &tmp))
644                 {
645                     if (tmp)
646                         retval->RereadLogAfterVerify = LOGMSG_REREAD_ALWAYS;
647                     else
648                         retval->RereadLogAfterVerify = LOGMSG_REREAD_NEVER;
649                 }
650             }
651         }
652         else if (strcmp (line, "TmpDir") == 0)
653         {
654             if (retval->TmpDir) free (retval->TmpDir);
655             retval->TmpDir = expand_path (p, cvsroot, false, infopath, ln);
656             /* Could try some validity checking, like whether we can
657              * opendir it or something, but I don't see any particular
658              * reason to do that now rather than when the first function
659              * tries to create a temp file.
660              */
661         }
662         else if (strcmp (line, "UserAdminOptions") == 0)
663             retval->UserAdminOptions = xstrdup (p);
664         else if (strcmp (line, "UseNewInfoFmtStrings") == 0)
665 #ifdef SUPPORT_OLD_INFO_FMT_STRINGS
666             readBool (infopath, "UseNewInfoFmtStrings", p,
667                       &retval->UseNewInfoFmtStrings);
668 #else /* !SUPPORT_OLD_INFO_FMT_STRINGS */
669         {
670             bool dummy;
671             if (readBool (infopath, "UseNewInfoFmtStrings", p, &dummy)
672                 && !dummy)
673                 error (1, 0,
674 "%s [%u]: Old style info format strings not supported by this executable.",
675                        infopath, ln);
676         }
677 #endif /* SUPPORT_OLD_INFO_FMT_STRINGS */
678         else if (strcmp (line, "ImportNewFilesToVendorBranchOnly") == 0)
679             readBool (infopath, "ImportNewFilesToVendorBranchOnly", p,
680                       &retval->ImportNewFilesToVendorBranchOnly);
681         else if (strcmp (line, "PrimaryServer") == 0)
682             retval->PrimaryServer = parse_cvsroot (p);
683 #ifdef PROXY_SUPPORT
684         else if (!strcmp (line, "MaxProxyBufferSize"))
685             readSizeT (infopath, "MaxProxyBufferSize", p,
686                        &retval->MaxProxyBufferSize);
687 #endif /* PROXY_SUPPORT */
688         else if (!strcmp (line, "MaxCommentLeaderLength"))
689             readSizeT (infopath, "MaxCommentLeaderLength", p,
690                        &retval->MaxCommentLeaderLength);
691         else if (!strcmp (line, "UseArchiveCommentLeader"))
692             readBool (infopath, "UseArchiveCommentLeader", p,
693                       &retval->UseArchiveCommentLeader);
694 #ifdef SERVER_SUPPORT
695         else if (!strcmp (line, "MinCompressionLevel")) {
696             readSizeT (infopath, "MinCompressionLevel", p, &dummy_sizet);
697             retval->MinCompressionLevel = dummy_sizet;
698         }
699         else if (!strcmp (line, "MaxCompressionLevel")) {
700             readSizeT (infopath, "MaxCompressionLevel", p, &dummy_sizet);
701             retval->MaxCompressionLevel = dummy_sizet;
702         }
703 #endif /* SERVER_SUPPORT */
704 #if !defined(LOCK_COMPATIBILITY) || !defined(SUPPORT_OLD_INFO_FMT_STRINGS)
705         else if ((!strcmp (line, "tag")) || (!strcmp (line, "umask"))
706           || (!strcmp (line, "DisableXProg")) || (!strcmp (line, "dlimit"))
707           || (!strcmp (line, "forceReadOnlyFS"))) {
708             /* We are dealing with keywords removed between cvs 1.11.1p1
709                and cvs 1.12.10; odds are we are not being able to handle
710                access or concurrent access with 1.11 cvs correctly */
711             error (0, 0, "%s: found keyword '%s' in repository",
712                    infopath, line);
713             error (readonlyfs ? 0 : 1, 0, readonlyfs
714                 ? "Danger: Granting read access to incompatible repository!"
715                 : "Do not try to access a cvs 1.11 repository!");
716         }
717 #endif
718         else
719             /* We may be dealing with a keyword which was added in a
720                subsequent version of CVS.  In that case it is a good idea
721                to complain, as (1) the keyword might enable a behavior like
722                alternate locking behavior, in which it is dangerous and hard
723                to detect if some CVS's have it one way and others have it
724                the other way, (2) in general, having us not do what the user
725                had in mind when they put in the keyword violates the
726                principle of least surprise.  Note that one corollary is
727                adding new keywords to your CVSROOT/config file is not
728                particularly recommended unless you are planning on using
729                the new features.  */
730             if (!parse_error (infopath, ln))
731                 error (0, 0, "%s [%u]: unrecognized keyword `%s'",
732                        infopath, ln, line);
733     }
734     if (ferror (fp_info))
735         error (0, errno, "cannot read %s", infopath);
736     if (fclose (fp_info) < 0)
737         error (0, errno, "cannot close %s", infopath);
738     if (freeinfopath) free (freeinfopath);
739     if (buf) free (buf);
740
741     return retval;
742 }