summaryrefslogtreecommitdiff
path: root/lib/chdir-long.c
blob: cb9b7d725f5f1fe8e66166ceeee53ba6826f2a97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
/* provide a chdir function that tries not to fail due to ENAMETOOLONG
   Copyright (C) 2004 Free Software Foundation, Inc.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

/* written by Jim Meyering */

#include <config.h>

#include "chdir-long.h"

#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <assert.h>
#include <limits.h>

#include "mempcpy.h"
#include "openat.h"

#ifndef O_DIRECTORY
# define O_DIRECTORY 0
#endif

#ifndef MIN
# define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif

#ifndef PATH_MAX
# ifdef	MAXPATHLEN
#  define PATH_MAX MAXPATHLEN
# else
#  error "use this module only if your system defines PATH_MAX"
# endif
#endif

/* FIXME: this use of `MIN' is our sole concession to arbitrary limitations.
   If, for some system, PATH_MAX is larger than 8191 and you call
   chdir_long with a directory name that is longer than PATH_MAX,
   yet that contains a single component that is more than 8191 bytes
   long, then this function will fail.  */
#define MAX_COMPONENT_LENGTH MIN (PATH_MAX - 1, 8 * 1024)

struct cd_buf
{
  /* FIXME maybe allocate this via malloc, rather than using the stack.
     But that would be the sole use of malloc.  Is it worth it to
     let chdir_long fail due to a low-memory condition?
     But when using malloc, and assuming we remove the `concession'
     above, we'll still have to avoid allocating 2^31 bytes on
     systems that define PATH_MAX to very large number.
     Ideally, we'd allocate enough to deal with most names, and
     dynamically increase the buffer size only necessary.  */
  char buffer[MAX_COMPONENT_LENGTH + 1];
  char *avail;
  int fd;
};

/* Like memchr, but return the number of bytes from MEM
   to the first occurrence of C thereafter.  Search only
   LEN bytes.  Return LEN if C is not found.  */
static inline size_t
memchrcspn (char const *mem, int c, size_t len)
{
  char const *found = memchr (mem, c, len);
  if (!found)
    return len;

  len = found - mem;
  return len;
}

static void
cdb_init (struct cd_buf *cdb)
{
  cdb->avail = cdb->buffer;
  cdb->fd = AT_FDCWD;
}

static inline bool
cdb_empty (struct cd_buf const *cdb)
{
  return cdb->avail == cdb->buffer;
}

static inline int
cdb_fchdir (struct cd_buf const *cdb)
{
  return fchdir (cdb->fd);
}

static int
cdb_advance_fd (struct cd_buf *cdb, char const *dir)
{
  int new_fd = openat (cdb->fd, dir, O_RDONLY | O_DIRECTORY);
  if (new_fd < 0)
    {
      new_fd = openat (cdb->fd, dir, O_WRONLY | O_DIRECTORY);
      if (new_fd < 0)
	return -1;
    }

  if (cdb->fd != AT_FDCWD)
    close (cdb->fd);
  cdb->fd = new_fd;

  return 0;
}

static int
cdb_flush (struct cd_buf *cdb)
{
  if (cdb_empty (cdb))
    return 0;

  cdb->avail[0] = '\0';
  if (cdb_advance_fd (cdb, cdb->buffer) != 0)
    return -1;

  cdb->avail = cdb->buffer;

  return 0;
}

static void
cdb_free (struct cd_buf *cdb)
{
  if (0 <= cdb->fd && close (cdb->fd) != 0)
    abort ();
}

static int
cdb_append (struct cd_buf *cdb, char const *s, size_t len)
{
  char const *end = cdb->buffer + sizeof cdb->buffer;

  /* Insert a slash separator if there is a preceding byte
     and it's not a slash.  */
  bool need_slash = (cdb->buffer < cdb->avail && cdb->avail[-1] != '/');
  size_t n_free;

  if (sizeof cdb->buffer < len + 1)
    {
      /* This single component is too long.  */
      errno = ENAMETOOLONG;
      return -1;
    }

  /* See if there's enough room for the `/', the new component and
     a trailing NUL.  */
  n_free = end - cdb->avail;
  if (n_free < need_slash + len + 1)
    {
      if (cdb_flush (cdb) != 0)
	return -1;
      need_slash = false;
    }

  if (need_slash)
    *(cdb->avail)++ = '/';

  cdb->avail = mempcpy (cdb->avail, s, len);
  return 0;
}

/* This is a wrapper around chdir that works even on PATH_MAX-limited
   systems.  It handles an arbitrarily long directory name by extracting
   and processing manageable portions of the name.  On systems without
   the openat syscall, this means changing the working directory to
   more and more `distant' points along the long directory name and
   then restoring the working directory.
   If any of those attempts to change or restore the working directory
   fails, this function exits nonzero.

   Note that this function may still fail with errno == ENAMETOOLONG,
   but only if the specified directory name contains a component that
   is long enough to provoke such a failure all by itself (e.g. if the
   component is longer than PATH_MAX on systems that define PATH_MAX).  */

int
chdir_long (char const *dir)
{
  int e = chdir (dir);
  if (e == 0 || errno != ENAMETOOLONG)
    return e;

  {
    size_t len = strlen (dir);
    char const *dir_end = dir + len;
    char const *d;
    struct cd_buf cdb;

    cdb_init (&cdb);

    /* If DIR is the empty string, then the chdir above
       must have failed and set errno to ENOENT.  */
    assert (0 < len);

    if (*dir == '/')
      {
	/* Names starting with exactly two slashes followed by at least
	   one non-slash are special --
	   for example, in some environments //Hostname/file may
	   denote a file on a different host.
	   Preserve those two leading slashes.  Treat all other
	   sequences of slashes like a single one.  */
	if (3 <= len && dir[1] == '/' && dir[2] != '/')
	  {
	    size_t name_len = 1 + strcspn (dir + 3, "/");
	    if (cdb_append (&cdb, dir, 2 + name_len) != 0)
	      goto Fail;
	    /* Advance D to next slash or to end of string. */
	    d = dir + 2 + name_len;
	    assert (*d == '/' || *d == '\0');
	  }
	else
	  {
	    if (cdb_append (&cdb, "/", 1) != 0)
	      goto Fail;
	    d = dir + 1;
	  }
      }
    else
      {
	d = dir;
      }

    while (1)
      {
	/* Skip any slashes to find start of next component --
	   or the end of DIR. */
	char const *start = d + strspn (d, "/");
	if (*start == '\0')
	  {
	    if (cdb_flush (&cdb) != 0)
	      goto Fail;
	    break;
	  }
	/* If the remaining portion is no longer than PATH_MAX, then
	   flush anything that is buffered and do the rest in one chunk.  */
	if (dir_end - start <= PATH_MAX)
	  {
	    if (cdb_flush (&cdb) != 0
		|| cdb_advance_fd (&cdb, start) != 0)
	      goto Fail;
	    break;
	  }

	len = memchrcspn (start, '/', dir_end - start);
	assert (len == strcspn (start, "/"));
	d = start + len;
	if (cdb_append (&cdb, start, len) != 0)
	  goto Fail;
      }

    if (cdb_fchdir (&cdb) != 0)
      goto Fail;

    cdb_free (&cdb);
    return 0;

   Fail:
    {
      int saved_errno = errno;
      cdb_free (&cdb);
      errno = saved_errno;
      return -1;
    }
  }
}

#if TEST_CHDIR

# include <stdio.h>
# include "closeout.h"
# include "error.h"

char *program_name;

int
main (int argc, char *argv[])
{
  char *line = NULL;
  size_t n = 0;
  int len;

  program_name = argv[0];
  atexit (close_stdout);

  len = getline (&line, &n, stdin);
  if (len < 0)
    {
      int saved_errno = errno;
      if (feof (stdin))
	exit (0);

      error (EXIT_FAILURE, saved_errno,
	     "reading standard input");
    }
  else if (len == 0)
    exit (0);

  if (line[len-1] == '\n')
    line[len-1] = '\0';

  if (chdir_long (line) != 0)
    error (EXIT_FAILURE, errno,
	   "chdir_long failed: %s", line);

  {
    /* Using `pwd' here makes sense only if it is a robust implementation,
       like the one in coreutils after the 2004-04-19 changes.  */
    char const *cmd = "pwd";
    execlp (cmd, (char *) NULL);
    error (EXIT_FAILURE, errno, "%s", cmd);
  }

  /* not reached */
  abort ();
}
#endif

/*
Local Variables:
compile-command: "gcc -DTEST_CHDIR=1 -DHAVE_CONFIG_H -I.. -g -O -W -Wall chdir-long.c libfetish.a"
End:
*/