Annotation of src/usr.bin/cvs/util.c, Revision 1.24
1.24 ! joris 1: /* $OpenBSD: util.c,v 1.23 2005/04/18 21:02:50 jfb Exp $ */
1.1 jfb 2: /*
3: * Copyright (c) 2004 Jean-Francois Brousseau <jfb@openbsd.org>
1.9 jfb 4: * All rights reserved.
1.1 jfb 5: *
1.9 jfb 6: * Redistribution and use in source and binary forms, with or without
7: * modification, are permitted provided that the following conditions
8: * are met:
1.1 jfb 9: *
1.9 jfb 10: * 1. Redistributions of source code must retain the above copyright
11: * notice, this list of conditions and the following disclaimer.
1.1 jfb 12: * 2. The name of the author may not be used to endorse or promote products
1.9 jfb 13: * derived from this software without specific prior written permission.
1.1 jfb 14: *
15: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
16: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
17: * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
18: * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23: * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
1.9 jfb 24: * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1.1 jfb 25: */
26:
27: #include <sys/types.h>
28: #include <sys/stat.h>
1.12 krapht 29: #include <sys/wait.h>
1.1 jfb 30:
31: #include <md5.h>
32: #include <errno.h>
1.2 jfb 33: #include <fcntl.h>
1.1 jfb 34: #include <stdio.h>
35: #include <stdlib.h>
36: #include <unistd.h>
37: #include <string.h>
38:
39: #include "cvs.h"
40: #include "log.h"
1.5 jfb 41: #include "file.h"
1.1 jfb 42:
1.9 jfb 43: static const char *cvs_months[] = {
44: "Jan", "Feb", "Mar", "Apr", "May", "Jun",
45: "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
46: };
47:
1.1 jfb 48:
49: /* letter -> mode type map */
50: static const int cvs_modetypes[26] = {
51: -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, -1,
52: -1, 2, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1,
53: };
54:
55: /* letter -> mode map */
56: static const mode_t cvs_modes[3][26] = {
57: {
58: 0, 0, 0, 0, 0, 0, 0, /* a - g */
59: 0, 0, 0, 0, 0, 0, 0, /* h - m */
60: 0, 0, 0, S_IRUSR, 0, 0, 0, /* n - u */
61: 0, S_IWUSR, S_IXUSR, 0, 0 /* v - z */
62: },
63: {
64: 0, 0, 0, 0, 0, 0, 0, /* a - g */
65: 0, 0, 0, 0, 0, 0, 0, /* h - m */
66: 0, 0, 0, S_IRGRP, 0, 0, 0, /* n - u */
67: 0, S_IWGRP, S_IXGRP, 0, 0 /* v - z */
68: },
69: {
70: 0, 0, 0, 0, 0, 0, 0, /* a - g */
71: 0, 0, 0, 0, 0, 0, 0, /* h - m */
72: 0, 0, 0, S_IROTH, 0, 0, 0, /* n - u */
73: 0, S_IWOTH, S_IXOTH, 0, 0 /* v - z */
74: }
75: };
76:
77:
78: /* octal -> string */
79: static const char *cvs_modestr[8] = {
80: "", "x", "w", "wx", "r", "rx", "rw", "rwx"
81: };
82:
83:
1.11 krapht 84: pid_t cvs_exec_pid;
85:
1.1 jfb 86:
87: /*
88: * cvs_readrepo()
89: *
90: * Read the path stored in the `Repository' CVS file for a given directory
91: * <dir>, and store that path into the buffer pointed to by <dst>, whose size
92: * is <len>.
93: */
94: int
95: cvs_readrepo(const char *dir, char *dst, size_t len)
96: {
1.21 xsa 97: int l;
1.1 jfb 98: size_t dlen;
99: FILE *fp;
100: char repo_path[MAXPATHLEN];
101:
1.21 xsa 102: l = snprintf(repo_path, sizeof(repo_path), "%s/CVS/Repository", dir);
103: if (l == -1 || l >= (int)sizeof(repo_path)) {
104: errno = ENAMETOOLONG;
105: cvs_log(LP_ERRNO, "%s", repo_path);
106: return (NULL);
107: }
108:
1.1 jfb 109: fp = fopen(repo_path, "r");
1.21 xsa 110: if (fp == NULL)
1.1 jfb 111: return (-1);
112:
113: if (fgets(dst, (int)len, fp) == NULL) {
114: if (ferror(fp)) {
115: cvs_log(LP_ERRNO, "failed to read from `%s'",
116: repo_path);
117: }
118: (void)fclose(fp);
119: return (-1);
120: }
121: dlen = strlen(dst);
122: if ((dlen > 0) && (dst[dlen - 1] == '\n'))
123: dst[--dlen] = '\0';
124:
125: (void)fclose(fp);
126: return (0);
127: }
128:
129:
130: /*
1.9 jfb 131: * cvs_datesec()
132: *
1.10 jfb 133: * Take a date string and transform it into the number of seconds since the
134: * Epoch. The <type> argument specifies whether the timestamp is in ctime(3)
135: * format or RFC 822 format (as CVS uses in its protocol). If the <adj>
136: * parameter is not 0, the returned time will be adjusted according to the
137: * machine's local timezone.
1.9 jfb 138: */
139: time_t
1.10 jfb 140: cvs_datesec(const char *date, int type, int adj)
1.9 jfb 141: {
142: int i;
143: long off;
144: char sign, mon[8], gmt[8], hr[4], min[4], *ep;
145: struct tm cvs_tm;
146:
147: memset(&cvs_tm, 0, sizeof(cvs_tm));
148: cvs_tm.tm_isdst = -1;
149:
1.10 jfb 150: if (type == CVS_DATE_RFC822) {
151: if (sscanf(date, "%d %3s %d %2d:%2d:%2d %5s", &cvs_tm.tm_mday,
152: mon, &cvs_tm.tm_year, &cvs_tm.tm_hour, &cvs_tm.tm_min,
153: &cvs_tm.tm_sec, gmt) < 7)
154: return (-1);
155: cvs_tm.tm_year -= 1900;
1.9 jfb 156:
1.10 jfb 157: if (*gmt == '-') {
158: sscanf(gmt, "%c%2s%2s", &sign, hr, min);
159: cvs_tm.tm_gmtoff = strtol(hr, &ep, 10);
1.9 jfb 160: if ((cvs_tm.tm_gmtoff == LONG_MIN) ||
161: (cvs_tm.tm_gmtoff == LONG_MAX) ||
162: (*ep != '\0')) {
163: cvs_log(LP_ERR,
1.10 jfb 164: "parse error in GMT hours specification `%s'", hr);
165: cvs_tm.tm_gmtoff = 0;
1.15 deraadt 166: } else {
1.10 jfb 167: /* get seconds */
168: cvs_tm.tm_gmtoff *= 3600;
169:
170: /* add the minutes */
171: off = strtol(min, &ep, 10);
172: if ((cvs_tm.tm_gmtoff == LONG_MIN) ||
173: (cvs_tm.tm_gmtoff == LONG_MAX) ||
174: (*ep != '\0')) {
175: cvs_log(LP_ERR,
176: "parse error in GMT minutes "
177: "specification `%s'", min);
1.15 deraadt 178: } else
1.10 jfb 179: cvs_tm.tm_gmtoff += off * 60;
1.9 jfb 180: }
181: }
1.10 jfb 182: if (sign == '-')
183: cvs_tm.tm_gmtoff = -cvs_tm.tm_gmtoff;
1.15 deraadt 184: } else if (type == CVS_DATE_CTIME) {
1.10 jfb 185: /* gmt is used for the weekday */
186: sscanf(date, "%3s %3s %d %2d:%2d:%2d %d", gmt, mon,
187: &cvs_tm.tm_mday, &cvs_tm.tm_hour, &cvs_tm.tm_min,
188: &cvs_tm.tm_sec, &cvs_tm.tm_year);
189: cvs_tm.tm_year -= 1900;
1.18 tedu 190: cvs_tm.tm_gmtoff = 0;
1.9 jfb 191: }
192:
193: for (i = 0; i < (int)(sizeof(cvs_months)/sizeof(cvs_months[0])); i++) {
194: if (strcmp(cvs_months[i], mon) == 0) {
195: cvs_tm.tm_mon = i;
196: break;
197: }
198: }
199:
200: return mktime(&cvs_tm);
201: }
202:
203:
204: /*
1.1 jfb 205: * cvs_strtomode()
206: *
207: * Read the contents of the string <str> and generate a permission mode from
208: * the contents of <str>, which is assumed to have the mode format of CVS.
209: * The CVS protocol specification states that any modes or mode types that are
210: * not recognized should be silently ignored. This function does not return
211: * an error in such cases, but will issue warnings.
212: * Returns 0 on success, or -1 on failure.
213: */
214: int
215: cvs_strtomode(const char *str, mode_t *mode)
216: {
1.2 jfb 217: char type;
1.1 jfb 218: mode_t m;
219: char buf[32], ms[4], *sp, *ep;
220:
221: m = 0;
222: strlcpy(buf, str, sizeof(buf));
223: sp = buf;
224: ep = sp;
225:
226: for (sp = buf; ep != NULL; sp = ep + 1) {
227: ep = strchr(sp, ',');
228: if (ep != NULL)
1.2 jfb 229: *ep = '\0';
1.1 jfb 230:
1.14 weingart 231: memset(ms, 0, sizeof ms);
232: if (sscanf(sp, "%c=%3s", &type, ms) != 2 &&
233: sscanf(sp, "%c=", &type) != 1) {
1.1 jfb 234: cvs_log(LP_WARN, "failed to scan mode string `%s'", sp);
235: continue;
236: }
237:
238: if ((type <= 'a') || (type >= 'z') ||
239: (cvs_modetypes[type - 'a'] == -1)) {
240: cvs_log(LP_WARN,
241: "invalid mode type `%c'"
242: " (`u', `g' or `o' expected), ignoring", type);
243: continue;
244: }
245:
246: /* make type contain the actual mode index */
247: type = cvs_modetypes[type - 'a'];
248:
249: for (sp = ms; *sp != '\0'; sp++) {
250: if ((*sp <= 'a') || (*sp >= 'z') ||
1.5 jfb 251: (cvs_modes[(int)type][*sp - 'a'] == 0)) {
1.1 jfb 252: cvs_log(LP_WARN,
253: "invalid permission bit `%c'", *sp);
1.15 deraadt 254: } else
1.5 jfb 255: m |= cvs_modes[(int)type][*sp - 'a'];
1.1 jfb 256: }
257: }
258:
259: *mode = m;
260:
261: return (0);
262: }
263:
264:
265: /*
266: * cvs_modetostr()
267: *
268: * Returns 0 on success, or -1 on failure.
269: */
270: int
271: cvs_modetostr(mode_t mode, char *buf, size_t len)
272: {
273: size_t l;
274: char tmp[16], *bp;
275: mode_t um, gm, om;
276:
277: um = (mode & S_IRWXU) >> 6;
278: gm = (mode & S_IRWXG) >> 3;
279: om = mode & S_IRWXO;
280:
281: bp = buf;
282: *bp = '\0';
283: l = 0;
284:
285: if (um) {
286: snprintf(tmp, sizeof(tmp), "u=%s", cvs_modestr[um]);
1.9 jfb 287: l = strlcat(buf, tmp, len);
1.1 jfb 288: }
289: if (gm) {
290: if (um)
291: strlcat(buf, ",", len);
292: snprintf(tmp, sizeof(tmp), "g=%s", cvs_modestr[gm]);
1.9 jfb 293: strlcat(buf, tmp, len);
1.1 jfb 294: }
295: if (om) {
296: if (um || gm)
297: strlcat(buf, ",", len);
298: snprintf(tmp, sizeof(tmp), "o=%s", cvs_modestr[gm]);
1.9 jfb 299: strlcat(buf, tmp, len);
1.1 jfb 300: }
301:
302: return (0);
303: }
304:
305:
306: /*
307: * cvs_cksum()
308: *
309: * Calculate the MD5 checksum of the file whose path is <file> and generate
310: * a CVS-format 32 hex-digit string, which is stored in <dst>, whose size is
311: * given in <len> and must be at least 33.
312: * Returns 0 on success, or -1 on failure.
313: */
314: int
315: cvs_cksum(const char *file, char *dst, size_t len)
316: {
317: if (len < CVS_CKSUM_LEN) {
318: cvs_log(LP_WARN, "buffer too small for checksum");
319: return (-1);
320: }
321: if (MD5File(file, dst) == NULL) {
1.19 jfb 322: cvs_log(LP_ERRNO, "failed to generate checksum for %s", file);
1.1 jfb 323: return (-1);
324: }
325:
326: return (0);
327: }
328:
329:
330: /*
331: * cvs_splitpath()
332: *
1.7 jfb 333: * Split a path <path> into the base portion and the filename portion.
334: * The path is copied in <base> and the last delimiter is replaced by a NUL
335: * byte. The <file> pointer is set to point to the first character after
336: * that delimiter.
1.1 jfb 337: * Returns 0 on success, or -1 on failure.
338: */
339: int
1.7 jfb 340: cvs_splitpath(const char *path, char *base, size_t blen, char **file)
1.1 jfb 341: {
342: size_t rlen;
1.7 jfb 343: char *sp;
1.1 jfb 344:
1.7 jfb 345: if ((rlen = strlcpy(base, path, blen)) >= blen)
346: return (-1);
1.6 jfb 347:
1.7 jfb 348: while ((rlen > 0) && (base[rlen - 1] == '/'))
349: base[--rlen] = '\0';
350:
351: sp = strrchr(base, '/');
1.1 jfb 352: if (sp == NULL) {
1.7 jfb 353: strlcpy(base, "./", blen);
354: strlcat(base, path, blen);
355: sp = base + 1;
1.1 jfb 356: }
357:
1.7 jfb 358: *sp = '\0';
359: if (file != NULL)
360: *file = sp + 1;
1.1 jfb 361:
362: return (0);
363: }
364:
365:
366: /*
367: * cvs_getargv()
368: *
369: * Parse a line contained in <line> and generate an argument vector by
370: * splitting the line on spaces and tabs. The resulting vector is stored in
371: * <argv>, which can accept up to <argvlen> entries.
1.20 david 372: * Returns the number of arguments in the vector, or -1 if an error occurred.
1.1 jfb 373: */
374: int
375: cvs_getargv(const char *line, char **argv, int argvlen)
376: {
377: u_int i;
378: int argc, err;
379: char linebuf[256], qbuf[128], *lp, *cp, *arg;
380:
381: strlcpy(linebuf, line, sizeof(linebuf));
382: memset(argv, 0, sizeof(argv));
383: argc = 0;
384:
385: /* build the argument vector */
386: err = 0;
387: for (lp = linebuf; lp != NULL;) {
388: if (*lp == '"') {
389: /* double-quoted string */
390: lp++;
391: i = 0;
392: memset(qbuf, 0, sizeof(qbuf));
393: while (*lp != '"') {
1.16 jfb 394: if (*lp == '\\')
395: lp++;
1.1 jfb 396: if (*lp == '\0') {
397: cvs_log(LP_ERR, "no terminating quote");
398: err++;
399: break;
1.16 jfb 400: }
1.1 jfb 401:
1.9 jfb 402: qbuf[i++] = *lp++;
1.1 jfb 403: if (i == sizeof(qbuf)) {
404: err++;
405: break;
406: }
407: }
408:
409: arg = qbuf;
1.15 deraadt 410: } else {
1.1 jfb 411: cp = strsep(&lp, " \t");
412: if (cp == NULL)
413: break;
414: else if (*cp == '\0')
415: continue;
416:
417: arg = cp;
418: }
419:
1.16 jfb 420: if (argc == argvlen) {
421: err++;
422: break;
423: }
424:
1.1 jfb 425: argv[argc] = strdup(arg);
426: if (argv[argc] == NULL) {
427: cvs_log(LP_ERRNO, "failed to copy argument");
428: err++;
429: break;
430: }
431: argc++;
432: }
433:
434: if (err) {
435: /* ditch the argument vector */
436: for (i = 0; i < (u_int)argc; i++)
437: free(argv[i]);
438: argc = -1;
439: }
440:
441: return (argc);
1.17 jfb 442: }
443:
444:
445: /*
446: * cvs_makeargv()
447: *
1.20 david 448: * Allocate an argument vector large enough to accommodate for all the
1.17 jfb 449: * arguments found in <line> and return it.
450: */
451:
452: char**
453: cvs_makeargv(const char *line, int *argc)
454: {
455: int i, ret;
456: char *argv[1024], **copy;
457:
458: ret = cvs_getargv(line, argv, 1024);
459: if (ret == -1)
460: return (NULL);
461:
462: copy = (char **)malloc((ret + 1) * sizeof(char *));
463: if (copy == NULL) {
464: cvs_log(LP_ERRNO, "failed to allocate argument vector");
465: return (NULL);
466: }
467: memset(copy, 0, sizeof(copy));
468:
469: for (i = 0; i < ret; i++)
470: copy[i] = argv[i];
471: copy[ret] = NULL;
472:
473: *argc = ret;
474: return (copy);
1.1 jfb 475: }
476:
477:
478: /*
479: * cvs_freeargv()
480: *
481: * Free an argument vector previously generated by cvs_getargv().
482: */
483: void
484: cvs_freeargv(char **argv, int argc)
485: {
486: int i;
487:
488: for (i = 0; i < argc; i++)
1.16 jfb 489: if (argv[i] != NULL)
490: free(argv[i]);
1.2 jfb 491: }
492:
493:
494: /*
495: * cvs_mkadmin()
496: *
1.5 jfb 497: * Create the CVS administrative files within the directory <cdir>. If the
498: * files already exist, they are kept as is.
1.2 jfb 499: * Returns 0 on success, or -1 on failure.
500: */
501: int
1.13 jfb 502: cvs_mkadmin(CVSFILE *cdir, mode_t mode)
1.2 jfb 503: {
1.21 xsa 504: int l;
1.13 jfb 505: char dpath[MAXPATHLEN], path[MAXPATHLEN];
1.2 jfb 506: FILE *fp;
507: CVSENTRIES *ef;
1.5 jfb 508: struct stat st;
1.2 jfb 509: struct cvsroot *root;
510:
1.13 jfb 511: cvs_file_getpath(cdir, dpath, sizeof(dpath));
512:
1.21 xsa 513: l = snprintf(path, sizeof(path), "%s/" CVS_PATH_CVSDIR, dpath);
514: if (l == -1 || l >= (int)sizeof(path)) {
515: errno = ENAMETOOLONG;
516: cvs_log(LP_ERRNO, "%s", path);
517: return (-1);
518: }
519:
1.5 jfb 520: if ((mkdir(path, mode) == -1) && (errno != EEXIST)) {
1.2 jfb 521: cvs_log(LP_ERRNO, "failed to create directory %s", path);
522: return (-1);
523: }
524:
1.5 jfb 525: /* just create an empty Entries file */
1.13 jfb 526: ef = cvs_ent_open(dpath, O_WRONLY);
1.2 jfb 527: (void)cvs_ent_close(ef);
528:
1.23 jfb 529: root = cdir->cf_root;
1.21 xsa 530: l = snprintf(path, sizeof(path), "%s/" CVS_PATH_ROOTSPEC, dpath);
531: if (l == -1 || l >= (int)sizeof(path)) {
532: errno = ENAMETOOLONG;
533: cvs_log(LP_ERRNO, "%s", path);
534: return (-1);
535: }
536:
1.8 jfb 537: if ((root != NULL) && (stat(path, &st) != 0) && (errno == ENOENT)) {
1.5 jfb 538: fp = fopen(path, "w");
539: if (fp == NULL) {
540: cvs_log(LP_ERRNO, "failed to open %s", path);
541: return (-1);
542: }
543: if (root->cr_user != NULL) {
544: fprintf(fp, "%s", root->cr_user);
545: if (root->cr_pass != NULL)
546: fprintf(fp, ":%s", root->cr_pass);
547: if (root->cr_host != NULL)
548: putc('@', fp);
549: }
550:
551: if (root->cr_host != NULL) {
552: fprintf(fp, "%s", root->cr_host);
553: if (root->cr_dir != NULL)
554: putc(':', fp);
555: }
556: if (root->cr_dir)
557: fprintf(fp, "%s", root->cr_dir);
558: putc('\n', fp);
559: (void)fclose(fp);
1.2 jfb 560: }
561:
1.22 xsa 562: l = snprintf(path, sizeof(path), "%s/" CVS_PATH_REPOSITORY, dpath);
1.21 xsa 563: if (l == -1 || l >= (int)sizeof(path)) {
564: errno = ENAMETOOLONG;
565: cvs_log(LP_ERRNO, "%s", path);
566: return (-1);
567: }
568:
1.5 jfb 569: if ((stat(path, &st) != 0) && (errno == ENOENT) &&
1.23 jfb 570: (cdir->cf_repo != NULL)) {
1.2 jfb 571: fp = fopen(path, "w");
572: if (fp == NULL) {
573: cvs_log(LP_ERRNO, "failed to open %s", path);
574: return (-1);
575: }
1.23 jfb 576: fprintf(fp, "%s\n", cdir->cf_repo);
1.2 jfb 577: (void)fclose(fp);
578: }
579:
580: return (0);
1.11 krapht 581: }
582:
583:
584: /*
585: * cvs_exec()
586: */
587: int
588: cvs_exec(int argc, char **argv, int fds[3])
589: {
590: int ret;
591: pid_t pid;
592:
593: if ((pid = fork()) == -1) {
594: cvs_log(LP_ERRNO, "failed to fork");
595: return (-1);
596: } else if (pid == 0) {
597: execvp(argv[0], argv);
1.13 jfb 598: cvs_log(LP_ERRNO, "failed to exec %s", argv[0]);
599: exit(1);
1.11 krapht 600: }
601:
602: if (waitpid(pid, &ret, 0) == -1)
1.13 jfb 603: cvs_log(LP_ERRNO, "failed to waitpid");
1.11 krapht 604:
605: return (ret);
1.1 jfb 606: }
1.24 ! joris 607:
! 608: /*
! 609: * remove a directory tree from disk.
! 610: */
! 611: int
! 612: cvs_remove_dir(const char *path)
! 613: {
! 614: int l, ret;
! 615: DIR *dirp;
! 616: struct dirent *ent;
! 617: char fpath[MAXPATHLEN];
! 618:
! 619: if ((dirp = opendir(path)) == NULL) {
! 620: cvs_log(LP_ERRNO, "failed to open '%s'", path);
! 621: return (CVS_EX_FILE);
! 622: }
! 623:
! 624: while ((ent = readdir(dirp)) != NULL) {
! 625: if (!strcmp(ent->d_name, ".") ||
! 626: !strcmp(ent->d_name, ".."))
! 627: continue;
! 628:
! 629: l = snprintf(fpath, sizeof(fpath), "%s/%s", path, ent->d_name);
! 630: if (l == -1 || l >= (int)sizeof(fpath)) {
! 631: errno = ENAMETOOLONG;
! 632: cvs_log(LP_ERRNO, "%s", fpath);
! 633: closedir(dirp);
! 634: return (CVS_EX_FILE);
! 635: }
! 636:
! 637: if (ent->d_type == DT_DIR) {
! 638: if ((ret = cvs_remove_dir(fpath)) != CVS_EX_OK) {
! 639: closedir(dirp);
! 640: return (ret);
! 641: }
! 642: } else {
! 643: if ((unlink(fpath) == -1) && (errno != ENOENT))
! 644: cvs_log(LP_ERRNO, "failed to remove '%s'",
! 645: fpath);
! 646: }
! 647: }
! 648:
! 649: closedir(dirp);
! 650:
! 651: if ((rmdir(path) == -1) && (errno != ENOENT))
! 652: cvs_log(LP_ERRNO, "failed to remove '%s'", path);
! 653:
! 654: return (CVS_EX_OK);
! 655: }
! 656: