Consolidate all local CVS patches into the contrib sources.
[dragonfly.git] / contrib / cvs-1.12 / 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 (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 }