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