version 1.2, 2011/11/27 11:46:44 |
version 1.13, 2017/02/04 12:03:07 |
|
|
/* $Id$ */ |
/* $Id$ */ |
/* |
/* |
* Copyright (c) 2011 Kristaps Dzonsons <kristaps@bsd.lv> |
* Copyright (c) 2017 Michael Stapelberg <stapelberg@debian.org> |
|
* Copyright (c) 2017 Ingo Schwarze <schwarze@openbsd.org> |
* |
* |
* Permission to use, copy, modify, and distribute this software for any |
* Permission to use, copy, modify, and distribute this software for any |
* purpose with or without fee is hereby granted, provided that the above |
* purpose with or without fee is hereby granted, provided that the above |
* copyright notice and this permission notice appear in all copies. |
* copyright notice and this permission notice appear in all copies. |
* |
* |
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES |
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR |
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
*/ |
*/ |
#ifdef HAVE_CONFIG_H |
|
#include "config.h" |
#include "config.h" |
#endif |
|
|
|
#include <sys/param.h> |
#include <sys/types.h> |
|
#include <sys/socket.h> |
#include <sys/stat.h> |
#include <sys/stat.h> |
#include <sys/wait.h> |
|
|
|
#include <assert.h> |
#if HAVE_ERR |
#include <errno.h> |
#include <err.h> |
|
#endif |
#include <fcntl.h> |
#include <fcntl.h> |
#include <getopt.h> |
#if HAVE_FTS |
|
#include <fts.h> |
|
#else |
|
#include "compat_fts.h" |
|
#endif |
#include <stdio.h> |
#include <stdio.h> |
#include <stdlib.h> |
#include <stdlib.h> |
#include <string.h> |
#include <string.h> |
#include <unistd.h> |
#include <unistd.h> |
|
|
#ifdef __linux__ |
int process_manpage(int, int, const char *); |
# include <db_185.h> |
int process_tree(int, int); |
#else |
void run_mandocd(int, const char *) __attribute__((noreturn)); |
# include <db.h> |
ssize_t sock_fd_write(int, int, int, int); |
#endif |
void usage(void) __attribute__((noreturn)); |
|
|
#include "manpath.h" |
|
|
|
#define xstrlcpy(_dst, _src, _sz) \ |
void |
do if (strlcpy((_dst), (_src), (_sz)) >= (_sz)) { \ |
run_mandocd(int sockfd, const char *outtype) |
fprintf(stderr, "%s: Path too long", (_dst)); \ |
|
exit(EXIT_FAILURE); \ |
|
} while (/* CONSTCOND */0) |
|
|
|
#define xstrlcat(_dst, _src, _sz) \ |
|
do if (strlcat((_dst), (_src), (_sz)) >= (_sz)) { \ |
|
fprintf(stderr, "%s: Path too long", (_dst)); \ |
|
exit(EXIT_FAILURE); \ |
|
} while (/* CONSTCOND */0) |
|
|
|
static int indexhtml(char *); |
|
#if 0 |
|
static int jobstart(const char *, const char *, pid_t *); |
|
static int jobwait(pid_t); |
|
#endif |
|
static int manup(const struct manpaths *, const char *); |
|
static int mkpath(char *, mode_t, mode_t); |
|
static int treecpy(char *, char *); |
|
static int update(char *, char *); |
|
static void usage(void); |
|
|
|
static const char *progname; |
|
static int verbose; |
|
static int force; |
|
|
|
int |
|
main(int argc, char *argv[]) |
|
{ |
{ |
int ch; |
char sockfdstr[10]; |
char *aux, *base; |
|
const char *dir; |
|
struct manpaths dirs; |
|
extern char *optarg; |
|
extern int optind; |
|
|
|
progname = strrchr(argv[0], '/'); |
if (snprintf(sockfdstr, sizeof(sockfdstr), "%d", sockfd) == -1) |
if (progname == NULL) |
err(1, "snprintf"); |
progname = argv[0]; |
execlp("mandocd", "mandocd", "-T", outtype, sockfdstr, NULL); |
else |
err(1, "exec"); |
++progname; |
|
|
|
aux = base = NULL; |
|
dir = "/var/www/cache/man.cgi"; |
|
|
|
while (-1 != (ch = getopt(argc, argv, "fm:M:o:v"))) |
|
switch (ch) { |
|
case ('f'): |
|
force = 1; |
|
break; |
|
case ('m'): |
|
aux = optarg; |
|
break; |
|
case ('M'): |
|
base = optarg; |
|
break; |
|
case ('o'): |
|
dir = optarg; |
|
break; |
|
case ('v'): |
|
verbose++; |
|
break; |
|
default: |
|
usage(); |
|
return(EXIT_FAILURE); |
|
} |
|
|
|
argc -= optind; |
|
argv += optind; |
|
|
|
if (argc > 0) { |
|
usage(); |
|
return(EXIT_FAILURE); |
|
} |
|
|
|
memset(&dirs, 0, sizeof(struct manpaths)); |
|
manpath_parse(&dirs, base, aux); |
|
ch = manup(&dirs, dir); |
|
manpath_free(&dirs); |
|
return(ch ? EXIT_SUCCESS : EXIT_FAILURE); |
|
} |
} |
|
|
static void |
ssize_t |
usage(void) |
sock_fd_write(int fd, int fd0, int fd1, int fd2) |
{ |
{ |
|
struct msghdr msg; |
fprintf(stderr, "usage: %s " |
struct iovec iov; |
"[-fv] " |
union { |
"[-o path] " |
struct cmsghdr cmsghdr; |
"[-m manpath] " |
char control[CMSG_SPACE(3 * sizeof(int))]; |
"[-M manpath]\n", |
} cmsgu; |
progname); |
struct cmsghdr *cmsg; |
} |
int *walk; |
|
unsigned char dummy[1] = {'\0'}; |
|
|
/* |
iov.iov_base = dummy; |
* If "src" file doesn't exist (errors out), return -1. Otherwise, |
iov.iov_len = sizeof(dummy); |
* return 1 if "src" is newer (which also happens "dst" doesn't exist) |
|
* and 0 otherwise. |
|
*/ |
|
static int |
|
isnewer(const char *dst, const char *src) |
|
{ |
|
struct stat s1, s2; |
|
|
|
if (-1 == stat(src, &s1)) |
msg.msg_name = NULL; |
return(-1); |
msg.msg_namelen = 0; |
if (force) |
msg.msg_iov = &iov; |
return(1); |
msg.msg_iovlen = 1; |
|
|
return(-1 == stat(dst, &s2) ? 1 : s1.st_mtime > s2.st_mtime); |
msg.msg_control = cmsgu.control; |
} |
msg.msg_controllen = sizeof(cmsgu.control); |
|
|
/* |
cmsg = CMSG_FIRSTHDR(&msg); |
* Copy the contents of one file into another. |
cmsg->cmsg_len = CMSG_LEN(3 * sizeof(int)); |
* Returns 0 on failure, 1 on success. |
cmsg->cmsg_level = SOL_SOCKET; |
*/ |
cmsg->cmsg_type = SCM_RIGHTS; |
static int |
|
filecpy(const char *dst, const char *src) |
|
{ |
|
char buf[BUFSIZ]; |
|
int sfd, dfd, rc; |
|
ssize_t rsz, wsz; |
|
|
|
sfd = dfd = -1; |
walk = (int *)CMSG_DATA(cmsg); |
rc = 0; |
*(walk++) = fd0; |
|
*(walk++) = fd1; |
|
*(walk++) = fd2; |
|
|
if (-1 == (dfd = open(dst, O_CREAT|O_TRUNC|O_WRONLY, 0644))) { |
return sendmsg(fd, &msg, 0); |
perror(dst); |
|
goto out; |
|
} else if (-1 == (sfd = open(src, O_RDONLY, 0))) { |
|
perror(src); |
|
goto out; |
|
} |
|
|
|
while ((rsz = read(sfd, buf, BUFSIZ)) > 0) |
|
if (-1 == (wsz = write(dfd, buf, (size_t)rsz))) { |
|
perror(dst); |
|
goto out; |
|
} else if (wsz < rsz) { |
|
fprintf(stderr, "%s: Short write\n", dst); |
|
goto out; |
|
} |
|
|
|
if (rsz < 0) |
|
perror(src); |
|
else |
|
rc = 1; |
|
out: |
|
if (-1 != sfd) |
|
close(sfd); |
|
if (-1 != dfd) |
|
close(dfd); |
|
|
|
return(rc); |
|
} |
} |
|
|
#if 0 |
int |
/* |
process_manpage(int srv_fd, int dstdir_fd, const char *path) |
* Clean up existing child. |
|
* Return 1 if cleaned up fine (or none was started) and 0 otherwise. |
|
*/ |
|
static int |
|
jobwait(pid_t pid) |
|
{ |
{ |
int st; |
int in_fd, out_fd; |
|
|
if (-1 == pid) |
if ((in_fd = open(path, O_RDONLY)) == -1) { |
return(1); |
warn("open(%s)", path); |
|
return -1; |
if (-1 == waitpid(pid, &st, 0)) { |
|
perror(NULL); |
|
exit(EXIT_FAILURE); |
|
} |
} |
|
|
return(WIFEXITED(st) && 0 == WEXITSTATUS(st)); |
if ((out_fd = openat(dstdir_fd, path, |
} |
O_WRONLY | O_NOFOLLOW | O_CREAT | O_TRUNC, |
|
S_IRUSR | S_IWUSR | S_IRGRP | S_IRWXO)) == -1) { |
/* |
warn("openat(%s)", path); |
* Start a job (child process), first making sure that the prior one has |
close(in_fd); |
* finished. |
return -1; |
* Return 1 if the prior child exited and the new one started, else 0. |
|
*/ |
|
static int |
|
jobstart(const char *dst, const char *src, pid_t *pid) |
|
{ |
|
int fd; |
|
|
|
if ( ! jobwait(*pid)) |
|
return(0); |
|
|
|
if (-1 == (*pid = fork())) { |
|
perror(NULL); |
|
exit(EXIT_FAILURE); |
|
} else if (*pid > 0) |
|
return(1); |
|
|
|
if (-1 == (fd = open(dst, O_WRONLY|O_TRUNC|O_CREAT, 0644))) { |
|
perror(dst); |
|
exit(EXIT_FAILURE); |
|
} |
} |
|
|
if (-1 == dup2(fd, STDOUT_FILENO)) { |
if (sock_fd_write(srv_fd, in_fd, out_fd, STDERR_FILENO) < 0) { |
perror(NULL); |
warn("sendmsg"); |
exit(EXIT_FAILURE); |
return -1; |
} |
} |
|
|
execlp("mandoc", "mandoc", "-T", "html", |
close(in_fd); |
"-O", "fragment", |
close(out_fd); |
"-O", "man=man.cgi?expr=%N&sec=%S", |
return 0; |
src, (char *)NULL); |
|
|
|
perror("mandoc"); |
|
exit(EXIT_FAILURE); |
|
/* NOTREACHED */ |
|
} |
} |
#endif |
|
|
|
/* |
int |
* Pass over the recno database and re-create HTML pages if they're |
process_tree(int srv_fd, int dstdir_fd) |
* found to be out of date. |
|
* Returns -1 on fatal error, 1 on success. |
|
*/ |
|
static int |
|
indexhtml(char *dst) |
|
{ |
{ |
DB *db; |
FTS *ftsp; |
DBT key, val; |
FTSENT *entry; |
size_t sz; |
const char *argv[2]; |
int c, rc; |
const char *path; |
unsigned int fl; |
|
const char *f; |
|
char *d; |
|
char fname[MAXPATHLEN]; |
|
pid_t pid; |
|
|
|
sz = strlen(dst); |
argv[0] = "."; |
pid = -1; |
argv[1] = (char *)NULL; |
|
|
xstrlcpy(fname, dst, MAXPATHLEN); |
if ((ftsp = fts_open((char * const *)argv, |
xstrlcat(fname, "/mandoc.index", MAXPATHLEN); |
FTS_PHYSICAL | FTS_NOCHDIR, NULL)) == NULL) { |
|
warn("fts_open"); |
db = dbopen(fname, O_RDONLY, 0, DB_RECNO, NULL); |
return -1; |
if (NULL == db) { |
|
perror(fname); |
|
return(-1); |
|
} |
} |
|
|
fl = R_FIRST; |
while ((entry = fts_read(ftsp)) != NULL) { |
while (0 == (c = (*db->seq)(db, &key, &val, fl))) { |
path = entry->fts_path + 2; |
fl = R_NEXT; |
switch (entry->fts_info) { |
f = (const char *)val.data; |
case FTS_F: |
|
process_manpage(srv_fd, dstdir_fd, path); |
dst[(int)sz] = '\0'; |
|
|
|
xstrlcat(dst, "/", MAXPATHLEN); |
|
xstrlcat(dst, f, MAXPATHLEN); |
|
/*xstrlcat(dst, ".html", MAXPATHLEN);*/ |
|
|
|
if (-1 == (rc = isnewer(dst, f))) { |
|
fprintf(stderr, "%s: Manpage missing\n", f); |
|
break; |
break; |
} else if (0 == rc) |
case FTS_D: |
continue; |
case FTS_DP: |
|
|
d = strrchr(dst, '/'); |
|
assert(NULL != d); |
|
*d = '\0'; |
|
|
|
if (-1 == mkpath(dst, 0755, 0755)) { |
|
perror(dst); |
|
break; |
break; |
} |
default: |
|
warnx("%s: not a regular file", path); |
*d = '/'; |
|
|
|
if ( ! filecpy(dst, f)) |
|
break; |
break; |
|
} |
/*if ( ! jobstart(dst, f, &pid)) |
|
break;*/ |
|
if (verbose) |
|
printf("%s\n", dst); |
|
} |
} |
|
|
(*db->close)(db); |
fts_close(ftsp); |
|
return 0; |
if (c < 0) |
|
perror(fname); |
|
/*if ( ! jobwait(pid)) |
|
c = -1;*/ |
|
|
|
return(1 == c ? 1 : -1); |
|
} |
} |
|
|
/* |
int |
* Copy both recno and btree databases into the destination. |
main(int argc, char **argv) |
* Call in to begin recreating HTML files. |
|
* Return -1 on fatal error and 1 if the update went well. |
|
*/ |
|
static int |
|
update(char *dst, char *src) |
|
{ |
{ |
size_t dsz, ssz; |
const char *outtype; |
|
int srv_fds[2]; |
|
int dstdir_fd; |
|
int opt; |
|
pid_t pid; |
|
|
dsz = strlen(dst); |
outtype = "ascii"; |
ssz = strlen(src); |
while ((opt = getopt(argc, argv, "T:")) != -1) { |
|
switch (opt) { |
|
case 'T': |
|
outtype = optarg; |
|
break; |
|
default: |
|
usage(); |
|
} |
|
} |
|
|
xstrlcat(src, "/mandoc.db", MAXPATHLEN); |
if (argc > 0) { |
xstrlcat(dst, "/mandoc.db", MAXPATHLEN); |
argc -= optind; |
|
argv += optind; |
if ( ! filecpy(dst, src)) |
|
return(-1); |
|
if (verbose) |
|
printf("%s\n", dst); |
|
|
|
dst[(int)dsz] = src[(int)ssz] = '\0'; |
|
|
|
xstrlcat(src, "/mandoc.index", MAXPATHLEN); |
|
xstrlcat(dst, "/mandoc.index", MAXPATHLEN); |
|
|
|
if ( ! filecpy(dst, src)) |
|
return(-1); |
|
if (verbose) |
|
printf("%s\n", dst); |
|
|
|
dst[(int)dsz] = '\0'; |
|
|
|
return(indexhtml(dst)); |
|
} |
|
|
|
/* |
|
* See if btree or recno databases in the destination are out of date |
|
* with respect to a single manpath component. |
|
* Return -1 on fatal error, 0 if the source is no longer valid (and |
|
* shouldn't be listed), and 1 if the update went well. |
|
*/ |
|
static int |
|
treecpy(char *dst, char *src) |
|
{ |
|
size_t dsz, ssz; |
|
int rc; |
|
|
|
dsz = strlen(dst); |
|
ssz = strlen(src); |
|
|
|
xstrlcat(src, "/mandoc.index", MAXPATHLEN); |
|
xstrlcat(dst, "/mandoc.index", MAXPATHLEN); |
|
|
|
if (-1 == (rc = isnewer(dst, src))) |
|
return(0); |
|
|
|
dst[(int)dsz] = src[(int)ssz] = '\0'; |
|
|
|
if (1 == rc) |
|
return(update(dst, src)); |
|
|
|
xstrlcat(src, "/mandoc.db", MAXPATHLEN); |
|
xstrlcat(dst, "/mandoc.db", MAXPATHLEN); |
|
|
|
if (-1 == (rc = isnewer(dst, src))) |
|
return(0); |
|
else if (rc == 0) |
|
return(1); |
|
|
|
dst[(int)dsz] = src[(int)ssz] = '\0'; |
|
|
|
return(update(dst, src)); |
|
} |
|
|
|
/* |
|
* Update the destination's file-tree with respect to changes in the |
|
* source manpath components. |
|
* "Change" is defined by an updated index or btree database. |
|
* Returns 1 on success, 0 on failure. |
|
*/ |
|
static int |
|
manup(const struct manpaths *dirs, const char *dir) |
|
{ |
|
char dst[MAXPATHLEN], |
|
src[MAXPATHLEN]; |
|
const char *path; |
|
int i, c; |
|
size_t sz; |
|
FILE *f; |
|
|
|
xstrlcpy(dst, dir, MAXPATHLEN); |
|
xstrlcat(dst, "/etc", MAXPATHLEN); |
|
|
|
if (-1 == mkpath(dst, 0755, 0755)) { |
|
perror(dst); |
|
return(0); |
|
} |
} |
|
if (argc != 2) |
|
usage(); |
|
|
xstrlcat(dst, "/man.conf", MAXPATHLEN); |
if (socketpair(AF_LOCAL, SOCK_STREAM, AF_UNSPEC, srv_fds) == -1) |
|
err(1, "socketpair"); |
|
|
if (verbose) |
pid = fork(); |
printf("%s\n", dst); |
switch (pid) { |
|
case -1: |
if (NULL == (f = fopen(dst, "w"))) { |
err(1, "fork"); |
perror(dst); |
case 0: |
return(0); |
close(srv_fds[0]); |
|
run_mandocd(srv_fds[1], outtype); |
|
default: |
|
break; |
} |
} |
|
close(srv_fds[1]); |
|
|
xstrlcpy(dst, dir, MAXPATHLEN); |
if ((dstdir_fd = open(argv[1], O_RDONLY | O_DIRECTORY)) == -1) |
sz = strlen(dst); |
err(1, "open(%s)", argv[1]); |
|
|
for (i = 0; i < dirs->sz; i++) { |
if (chdir(argv[0]) == -1) |
path = dirs->paths[i]; |
err(1, "chdir(%s)", argv[0]); |
|
|
dst[(int)sz] = '\0'; |
return process_tree(srv_fds[0], dstdir_fd) == -1 ? 1 : 0; |
xstrlcat(dst, path, MAXPATHLEN); |
|
|
|
if (-1 == mkpath(dst, 0755, 0755)) { |
|
perror(dst); |
|
break; |
|
} |
|
|
|
xstrlcpy(src, path, MAXPATHLEN); |
|
|
|
if (-1 == (c = treecpy(dst, src))) |
|
break; |
|
else if (0 == c) |
|
continue; |
|
|
|
/* |
|
* We want to use a relative path here because manpath.h |
|
* will realpath() when invoked with man.cgi, and we'll |
|
* make sure to chdir() into the cache directory before. |
|
* |
|
* This allows the cache directory to be in an arbitrary |
|
* place, working in both chroot() and non-chroot() |
|
* "safe" modes. |
|
*/ |
|
assert('/' == path[0]); |
|
fprintf(f, "_whatdb %s/whatis.db\n", path + 1); |
|
} |
|
|
|
fclose(f); |
|
return(i == dirs->sz); |
|
} |
} |
|
|
/* |
void |
* Copyright (c) 1983, 1992, 1993 |
usage(void) |
* The Regents of the University of California. All rights reserved. |
|
* |
|
* Redistribution and use in source and binary forms, with or without |
|
* modification, are permitted provided that the following conditions |
|
* are met: |
|
* 1. Redistributions of source code must retain the above copyright |
|
* notice, this list of conditions and the following disclaimer. |
|
* 2. Redistributions in binary form must reproduce the above copyright |
|
* notice, this list of conditions and the following disclaimer in the |
|
* documentation and/or other materials provided with the distribution. |
|
* 3. Neither the name of the University nor the names of its contributors |
|
* may be used to endorse or promote products derived from this software |
|
* without specific prior written permission. |
|
* |
|
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND |
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE |
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS |
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
|
* SUCH DAMAGE. |
|
*/ |
|
static int |
|
mkpath(char *path, mode_t mode, mode_t dir_mode) |
|
{ |
{ |
struct stat sb; |
fprintf(stderr, "usage: catman [-T output] srcdir dstdir\n"); |
char *slash; |
exit(1); |
int done, exists; |
|
|
|
slash = path; |
|
|
|
for (;;) { |
|
/* LINTED */ |
|
slash += strspn(slash, "/"); |
|
/* LINTED */ |
|
slash += strcspn(slash, "/"); |
|
|
|
done = (*slash == '\0'); |
|
*slash = '\0'; |
|
|
|
/* skip existing path components */ |
|
exists = !stat(path, &sb); |
|
if (!done && exists && S_ISDIR(sb.st_mode)) { |
|
*slash = '/'; |
|
continue; |
|
} |
|
|
|
if (mkdir(path, done ? mode : dir_mode) == 0) { |
|
if (mode > 0777 && chmod(path, mode) < 0) |
|
return (-1); |
|
} else { |
|
if (!exists) { |
|
/* Not there */ |
|
return (-1); |
|
} |
|
if (!S_ISDIR(sb.st_mode)) { |
|
/* Is there, but isn't a directory */ |
|
errno = ENOTDIR; |
|
return (-1); |
|
} |
|
} |
|
|
|
if (done) |
|
break; |
|
|
|
*slash = '/'; |
|
} |
|
|
|
return (0); |
|
} |
} |