1/* pathchk - check pathnames for validity and portability */
2
3/* Usage: pathchk [-p] path ...
4
5   For each PATH, print a message if any of these conditions are false:
6   * all existing leading directories in PATH have search (execute) permission
7   * strlen (PATH) <= PATH_MAX
8   * strlen (each_directory_in_PATH) <= NAME_MAX
9
10   Exit status:
11   0			All PATH names passed all of the tests.
12   1			An error occurred.
13
14   Options:
15   -p			Instead of performing length checks on the
16			underlying filesystem, test the length of the
17			pathname and its components against the POSIX.1
18			minimum limits for portability, _POSIX_NAME_MAX
19			and _POSIX_PATH_MAX in 2.9.2.  Also check that
20			the pathname contains no character not in the
21			portable filename character set. */
22
23/* See Makefile for compilation details. */
24
25#include <config.h>
26
27#include <sys/types.h>
28#include "posixstat.h"
29
30#if defined (HAVE_UNISTD_H)
31#  include <unistd.h>
32#endif
33
34#if defined (HAVE_LIMITS_H)
35#  include <limits.h>
36#endif
37
38#include "bashansi.h"
39
40#include <stdio.h>
41#include <errno.h>
42
43#include "builtins.h"
44#include "shell.h"
45#include "stdc.h"
46#include "bashgetopt.h"
47#include "maxpath.h"
48
49#if !defined (errno)
50extern int errno;
51#endif
52
53#if !defined (_POSIX_PATH_MAX)
54#  define _POSIX_PATH_MAX 255
55#endif
56#if !defined (_POSIX_NAME_MAX)
57#  define _POSIX_NAME_MAX 14
58#endif
59
60/* How do we get PATH_MAX? */
61#if defined (_POSIX_VERSION) && !defined (PATH_MAX)
62#  define PATH_MAX_FOR(p) pathconf ((p), _PC_PATH_MAX)
63#endif
64
65/* How do we get NAME_MAX? */
66#if defined (_POSIX_VERSION) && !defined (NAME_MAX)
67#  define NAME_MAX_FOR(p) pathconf ((p), _PC_NAME_MAX)
68#endif
69
70#if !defined (PATH_MAX_FOR)
71#  define PATH_MAX_FOR(p)	PATH_MAX
72#endif
73
74#if !defined (NAME_MAX_FOR)
75#  define NAME_MAX_FOR(p)	NAME_MAX
76#endif
77
78extern char *strerror ();
79
80static int validate_path ();
81
82pathchk_builtin (list)
83     WORD_LIST *list;
84{
85  int retval, pflag, opt;
86
87  reset_internal_getopt ();
88  while ((opt = internal_getopt (list, "p")) != -1)
89    {
90      switch (opt)
91	{
92	case 'p':
93	  pflag = 1;
94	  break;
95	default:
96	  builtin_usage ();
97	  return (EX_USAGE);
98	}
99    }
100  list = loptend;
101
102  if (list == 0)
103    {
104      builtin_usage ();
105      return (EX_USAGE);
106    }
107
108  for (retval = 0; list; list = list->next)
109    retval |= validate_path (list->word->word, pflag);
110
111  return (retval ? EXECUTION_FAILURE : EXECUTION_SUCCESS);
112}
113
114char *pathchk_doc[] = {
115	"Check each pathname argument for validity (i.e., it may be used to",
116	"create or access a file without casuing syntax errors) and portability",
117	"(i.e., no filename truncation will result).  If the `-p' option is",
118	"supplied, more extensive portability checks are performed.",
119	(char *)NULL
120};
121
122/* The standard structure describing a builtin command.  bash keeps an array
123   of these structures. */
124struct builtin pathchk_struct = {
125	"pathchk",		/* builtin name */
126	pathchk_builtin,	/* function implementing the builtin */
127	BUILTIN_ENABLED,	/* initial flags for builtin */
128	pathchk_doc,		/* array of long documentation strings. */
129	"pathchk [-p] pathname ...",	/* usage synopsis */
130	0			/* reserved for internal use */
131};
132
133/* The remainder of this file is stolen shamelessly from `pathchk.c' in
134   the sh-utils-1.12 distribution, by
135
136   David MacKenzie <djm@gnu.ai.mit.edu>
137   and Jim Meyering <meyering@cs.utexas.edu> */
138
139/* Each element is nonzero if the corresponding ASCII character is
140   in the POSIX portable character set, and zero if it is not.
141   In addition, the entry for `/' is nonzero to simplify checking. */
142static char const portable_chars[256] =
143{
144  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 0-15 */
145  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16-31 */
146  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, /* 32-47 */
147  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, /* 48-63 */
148  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 64-79 */
149  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, /* 80-95 */
150  0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, /* 96-111 */
151  1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, /* 112-127 */
152  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
153  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
154  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
155  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
156  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
157  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
158  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
159  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
160};
161
162/* If PATH contains only portable characters, return 1, else 0.  */
163
164static int
165portable_chars_only (path)
166     const char *path;
167{
168  const char *p;
169
170  for (p = path; *p; ++p)
171    if (portable_chars[(const unsigned char) *p] == 0)
172      {
173	builtin_error ("path `%s' contains nonportable character `%c'", path, *p);
174	return 0;
175      }
176  return 1;
177}
178
179/* On some systems, stat can return EINTR.  */
180
181#ifndef EINTR
182# define SAFE_STAT(name, buf) stat (name, buf)
183#else
184# define SAFE_STAT(name, buf) safe_stat (name, buf)
185static inline int
186safe_stat (name, buf)
187     const char *name;
188     struct stat *buf;
189{
190  int ret;
191
192  do
193    ret = stat (name, buf);
194  while (ret < 0 && errno == EINTR);
195
196  return ret;
197}
198#endif
199
200/* Return 1 if PATH is a usable leading directory, 0 if not,
201   2 if it doesn't exist.  */
202
203static int
204dir_ok (path)
205     const char *path;
206{
207  struct stat stats;
208
209  if (SAFE_STAT (path, &stats))
210    return 2;
211
212  if (!S_ISDIR (stats.st_mode))
213    {
214      builtin_error ("`%s' is not a directory", path);
215      return 0;
216    }
217
218  /* Use access to test for search permission because
219     testing permission bits of st_mode can lose with new
220     access control mechanisms.  Of course, access loses if you're
221     running setuid. */
222  if (access (path, X_OK) != 0)
223    {
224      if (errno == EACCES)
225	builtin_error ("directory `%s' is not searchable", path);
226      else
227	builtin_error ("%s: %s", path, strerror (errno));
228      return 0;
229    }
230
231  return 1;
232}
233
234static char *
235xstrdup (s)
236     char *s;
237{
238  return (savestring (s));
239}
240
241/* Make sure that
242   strlen (PATH) <= PATH_MAX
243   && strlen (each-existing-directory-in-PATH) <= NAME_MAX
244
245   If PORTABILITY is nonzero, compare against _POSIX_PATH_MAX and
246   _POSIX_NAME_MAX instead, and make sure that PATH contains no
247   characters not in the POSIX portable filename character set, which
248   consists of A-Z, a-z, 0-9, ., _, -.
249
250   Make sure that all leading directories along PATH that exist have
251   `x' permission.
252
253   Return 0 if all of these tests are successful, 1 if any fail. */
254
255static int
256validate_path (path, portability)
257     char *path;
258     int portability;
259{
260  int path_max;
261  int last_elem;		/* Nonzero if checking last element of path. */
262  int exists;			/* 2 if the path element exists.  */
263  char *slash;
264  char *parent;			/* Last existing leading directory so far.  */
265
266  if (portability && !portable_chars_only (path))
267    return 1;
268
269  if (*path == '\0')
270    return 0;
271
272#ifdef lint
273  /* Suppress `used before initialized' warning.  */
274  exists = 0;
275#endif
276
277  /* Figure out the parent of the first element in PATH.  */
278  parent = xstrdup (*path == '/' ? "/" : ".");
279
280  slash = path;
281  last_elem = 0;
282  while (1)
283    {
284      int name_max;
285      int length;		/* Length of partial path being checked. */
286      char *start;		/* Start of path element being checked. */
287
288      /* Find the end of this element of the path.
289	 Then chop off the rest of the path after this element. */
290      while (*slash == '/')
291	slash++;
292      start = slash;
293      slash = strchr (slash, '/');
294      if (slash != NULL)
295	*slash = '\0';
296      else
297	{
298	  last_elem = 1;
299	  slash = strchr (start, '\0');
300	}
301
302      if (!last_elem)
303	{
304	  exists = dir_ok (path);
305	  if (dir_ok == 0)
306	    {
307	      free (parent);
308	      return 1;
309	    }
310	}
311
312      length = slash - start;
313      /* Since we know that `parent' is a directory, it's ok to call
314	 pathconf with it as the argument.  (If `parent' isn't a directory
315	 or doesn't exist, the behavior of pathconf is undefined.)
316	 But if `parent' is a directory and is on a remote file system,
317	 it's likely that pathconf can't give us a reasonable value
318	 and will return -1.  (NFS and tempfs are not POSIX . . .)
319	 In that case, we have no choice but to assume the pessimal
320	 POSIX minimums.  */
321      name_max = portability ? _POSIX_NAME_MAX : NAME_MAX_FOR (parent);
322      if (name_max < 0)
323	name_max = _POSIX_NAME_MAX;
324      if (length > name_max)
325	{
326	  builtin_error ("name `%s' has length %d; exceeds limit of %d",
327		 start, length, name_max);
328	  free (parent);
329	  return 1;
330	}
331
332      if (last_elem)
333	break;
334
335      if (exists == 1)
336	{
337	  free (parent);
338	  parent = xstrdup (path);
339	}
340
341      *slash++ = '/';
342    }
343
344  /* `parent' is now the last existing leading directory in the whole path,
345     so it's ok to call pathconf with it as the argument.  */
346  path_max = portability ? _POSIX_PATH_MAX : PATH_MAX_FOR (parent);
347  if (path_max < 0)
348    path_max = _POSIX_PATH_MAX;
349  free (parent);
350  if (strlen (path) > path_max)
351    {
352      builtin_error ("path `%s' has length %d; exceeds limit of %d",
353	     path, strlen (path), path_max);
354      return 1;
355    }
356
357  return 0;
358}
359