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