1/*	$NetBSD: pathfind.c,v 1.1.1.2 2012/01/31 21:27:55 kardel Exp $	*/
2
3/*  -*- Mode: C -*-  */
4
5/* pathfind.c --- find a FILE  MODE along PATH */
6
7/*
8 * Author:           Gary V Vaughan <gvaughan@oranda.demon.co.uk>
9 * Time-stamp:       "2010-07-17 09:50:32 bkorb"
10 */
11
12/* Code: */
13
14#include "compat.h"
15#ifndef HAVE_PATHFIND
16#if defined(__windows__) && !defined(__CYGWIN__)
17char*
18pathfind( char const*  path,
19          char const*  fileName,
20          char const*  mode )
21{
22    return NULL;
23}
24#else
25
26static char* make_absolute( char const *string, char const *dot_path );
27static char* canonicalize_pathname( char *path );
28static char* extract_colon_unit( char* dir, char const *string, int *p_index );
29
30
31/*=export_func pathfind
32 *
33 * what: fild a file in a list of directories
34 *
35 * ifndef: HAVE_PATHFIND
36 *
37 * arg:  + char const* + path + colon separated list of search directories +
38 * arg:  + char const* + file + the name of the file to look for +
39 * arg:  + char const* + mode + the mode bits that must be set to match +
40 *
41 * ret_type:  char*
42 * ret_desc:  the path to the located file
43 *
44 * doc:
45 *
46 * pathfind looks for a a file with name "FILE" and "MODE" access
47 * along colon delimited "PATH", and returns the full pathname as a
48 * string, or NULL if not found.  If "FILE" contains a slash, then
49 * it is treated as a relative or absolute path and "PATH" is ignored.
50 *
51 * @strong{NOTE}: this function is compiled into @file{libopts} only if
52 * it is not natively supplied.
53 *
54 * The "MODE" argument is a string of option letters chosen from the
55 * list below:
56 * @example
57 *          Letter    Meaning
58 *          r         readable
59 *          w         writable
60 *          x         executable
61 *          f         normal file       (NOT IMPLEMENTED)
62 *          b         block special     (NOT IMPLEMENTED)
63 *          c         character special (NOT IMPLEMENTED)
64 *          d         directory         (NOT IMPLEMENTED)
65 *          p         FIFO (pipe)       (NOT IMPLEMENTED)
66 *          u         set user ID bit   (NOT IMPLEMENTED)
67 *          g         set group ID bit  (NOT IMPLEMENTED)
68 *          k         sticky bit        (NOT IMPLEMENTED)
69 *          s         size nonzero      (NOT IMPLEMENTED)
70 * @end example
71 *
72 * example:
73 * To find the "ls" command using the "PATH" environment variable:
74 * @example
75 *    #include <stdlib.h>
76 *    char* pz_ls = pathfind( getenv("PATH"), "ls", "rx" );
77 *    <<do whatever with pz_ls>>
78 *    free( pz_ls );
79 * @end example
80 * The path is allocated with @code{malloc(3C)}, so you must @code{free(3C)}
81 * the result.  Also, do not use unimplemented file modes.  :-)
82 *
83 * err:  returns NULL if the file is not found.
84=*/
85char*
86pathfind( char const*  path,
87          char const*  fileName,
88          char const*  mode )
89{
90    int   p_index   = 0;
91    int   mode_bits = 0;
92    char* pathName  = NULL;
93    char  zPath[ AG_PATH_MAX + 1 ];
94
95    if (strchr( mode, 'r' )) mode_bits |= R_OK;
96    if (strchr( mode, 'w' )) mode_bits |= W_OK;
97    if (strchr( mode, 'x' )) mode_bits |= X_OK;
98
99    /*
100     *  FOR each non-null entry in the colon-separated path, DO ...
101     */
102    for (;;) {
103        DIR*  dirP;
104        char* colon_unit = extract_colon_unit( zPath, path, &p_index );
105
106        /*
107         *  IF no more entries, THEN quit
108         */
109        if (colon_unit == NULL)
110            break;
111
112        dirP = opendir( colon_unit );
113
114        /*
115         *  IF the directory is inaccessable, THEN next directory
116         */
117        if (dirP == NULL)
118            continue;
119
120        /*
121         *  FOR every entry in the given directory, ...
122         */
123        for (;;) {
124            struct dirent *entP = readdir( dirP );
125
126            if (entP == (struct dirent*)NULL)
127                break;
128
129            /*
130             *  IF the file name matches the one we are looking for, ...
131             */
132            if (strcmp( entP->d_name, fileName ) == 0) {
133                char* pzFullName = make_absolute( fileName, colon_unit);
134
135                /*
136                 *  Make sure we can access it in the way we want
137                 */
138                if (access( pzFullName, mode_bits ) >= 0) {
139                    /*
140                     *  We can, so normalize the name and return it below
141                     */
142                    pathName = canonicalize_pathname( pzFullName );
143                }
144
145                free( (void*)pzFullName );
146                break;
147            }
148        }
149
150        closedir( dirP );
151
152        if (pathName != NULL)
153            break;
154    }
155
156    return pathName;
157}
158
159/*
160 * Turn STRING  (a pathname) into an  absolute  pathname, assuming  that
161 * DOT_PATH contains the symbolic location of  `.'.  This always returns
162 * a new string, even if STRING was an absolute pathname to begin with.
163 */
164static char*
165make_absolute( char const *string, char const *dot_path )
166{
167    char *result;
168    int result_len;
169
170    if (!dot_path || *string == '/') {
171        result = strdup( string );
172    } else {
173        if (dot_path && dot_path[0]) {
174            result = malloc( 2 + strlen( dot_path ) + strlen( string ) );
175            strcpy( result, dot_path );
176            result_len = strlen( result );
177            if (result[result_len - 1] != '/') {
178                result[result_len++] = '/';
179                result[result_len] = '\0';
180            }
181        } else {
182            result = malloc( 3 + strlen( string ) );
183            result[0] = '.'; result[1] = '/'; result[2] = '\0';
184            result_len = 2;
185        }
186
187        strcpy( result + result_len, string );
188    }
189
190    return result;
191}
192
193/*
194 * Canonicalize PATH, and return a  new path.  The new path differs from
195 * PATH in that:
196 *
197 *    Multiple `/'s     are collapsed to a single `/'.
198 *    Leading `./'s     are removed.
199 *    Trailing `/.'s    are removed.
200 *    Trailing `/'s     are removed.
201 *    Non-leading `../'s and trailing `..'s are handled by removing
202 *                    portions of the path.
203 */
204static char*
205canonicalize_pathname( char *path )
206{
207    int i, start;
208    char stub_char, *result;
209
210    /* The result cannot be larger than the input PATH. */
211    result = strdup( path );
212
213    stub_char = (*path == '/') ? '/' : '.';
214
215    /* Walk along RESULT looking for things to compact. */
216    i = 0;
217    while (result[i]) {
218        while (result[i] != '\0' && result[i] != '/')
219            i++;
220
221        start = i++;
222
223        /* If we didn't find any  slashes, then there is nothing left to
224         * do.
225         */
226        if (!result[start])
227            break;
228
229        /* Handle multiple `/'s in a row. */
230        while (result[i] == '/')
231            i++;
232
233#if !defined (apollo)
234        if ((start + 1) != i)
235#else
236        if ((start + 1) != i && (start != 0 || i != 2))
237#endif /* apollo */
238        {
239            strcpy( result + start + 1, result + i );
240            i = start + 1;
241        }
242
243        /* Handle backquoted `/'. */
244        if (start > 0 && result[start - 1] == '\\')
245            continue;
246
247        /* Check for trailing `/', and `.' by itself. */
248        if ((start && !result[i])
249            || (result[i] == '.' && !result[i+1])) {
250            result[--i] = '\0';
251            break;
252        }
253
254        /* Check for `../', `./' or trailing `.' by itself. */
255        if (result[i] == '.') {
256            /* Handle `./'. */
257            if (result[i + 1] == '/') {
258                strcpy( result + i, result + i + 1 );
259                i = (start < 0) ? 0 : start;
260                continue;
261            }
262
263            /* Handle `../' or trailing `..' by itself. */
264            if (result[i + 1] == '.' &&
265                (result[i + 2] == '/' || !result[i + 2])) {
266                while (--start > -1 && result[start] != '/')
267                    ;
268                strcpy( result + start + 1, result + i + 2 );
269                i = (start < 0) ? 0 : start;
270                continue;
271            }
272        }
273    }
274
275    if (!*result) {
276        *result = stub_char;
277        result[1] = '\0';
278    }
279
280    return result;
281}
282
283/*
284 * Given a  string containing units of information separated  by colons,
285 * return the next one  pointed to by (P_INDEX), or NULL if there are no
286 * more.  Advance (P_INDEX) to the character after the colon.
287 */
288static char*
289extract_colon_unit( char* pzDir, char const *string, int *p_index )
290{
291    char*  pzDest = pzDir;
292    int    ix     = *p_index;
293
294    if (string == NULL)
295        return NULL;
296
297    if ((unsigned)ix >= strlen( string ))
298        return NULL;
299
300    {
301        char const* pzSrc = string + ix;
302
303        while (*pzSrc == ':')  pzSrc++;
304
305        for (;;) {
306            char ch = (*(pzDest++) = *(pzSrc++));
307            switch (ch) {
308            case ':':
309                pzDest[-1] = NUL;
310            case NUL:
311                goto copy_done;
312            }
313
314            if ((size_t)(pzDest - pzDir) >= AG_PATH_MAX)
315                break;
316        } copy_done:;
317
318        ix = pzSrc - string;
319    }
320
321    if (*pzDir == NUL)
322        return NULL;
323
324    *p_index = ix;
325    return pzDir;
326}
327#endif /* __windows__ / __CYGWIN__ */
328#endif /* HAVE_PATHFIND */
329
330/*
331 * Local Variables:
332 * mode: C
333 * c-file-style: "stroustrup"
334 * indent-tabs-mode: nil
335 * End:
336 * end of compat/pathfind.c */
337